diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index a3470cd750738..aafdf06433c6d 100644 --- a/.ci/es-snapshots/Jenkinsfile_build_es +++ b/.ci/es-snapshots/Jenkinsfile_build_es @@ -25,7 +25,7 @@ def PROMOTE_WITHOUT_VERIFY = !!params.PROMOTE_WITHOUT_VERIFICATION timeout(time: 120, unit: 'MINUTES') { timestamps { ansiColor('xterm') { - node(workers.label('s')) { + node(workers.label('l')) { catchErrors { def VERSION def SNAPSHOT_ID @@ -154,9 +154,10 @@ def buildArchives(destination) { "NODE_NAME=", ]) { sh """ - ./gradlew -p distribution/archives assemble --parallel + ./gradlew -Dbuild.docker=true assemble --parallel mkdir -p ${destination} - find distribution/archives -type f \\( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elasticsearch-*-*-*-*.zip' \\) -not -path *no-jdk* -exec cp {} ${destination} \\; + find distribution -type f \\( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elasticsearch-*-*-*-*.zip' \\) -not -path *no-jdk* -not -path *build-context* -exec cp {} ${destination} \\; + docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 bash -c 'docker save docker.elastic.co/elasticsearch/elasticsearch:\${0} | gzip > ${destination}/elasticsearch-\${0}-docker-image.tar.gz' """ } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7345f4b2897b..a0aeed7a34949 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -436,7 +436,7 @@ We are still to develop a proper process to accept any contributed translations. When writing a new component, create a sibling SASS file of the same name and import directly into the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). -Any JavaScript (or TypeScript) file that imports SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`styling_constants.scss` file](https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/styles/_styling_constants.scss). However, any Legacy (file path includes `/legacy`) files will not. +All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/legacy/ui/public/styles/_globals_v7light.scss). **Example:** @@ -679,15 +679,15 @@ Part of this process only applies to maintainers, since it requires access to Gi Kibana publishes [Release Notes](https://www.elastic.co/guide/en/kibana/current/release-notes.html) for major and minor releases. The Release Notes summarize what the PRs accomplish in language that is meaningful to users. To generate the Release Notes, the team runs a script against this repo to collect the merged PRs against the release. #### Create the Release Notes text -The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description. +The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description. To use a single paragraph of text, enter `Release note:` or a `## Release note` header in the PR description, followed by your text. For example, refer to this [PR](https://github.com/elastic/kibana/pull/65796) that uses the `## Release note` header. When you create the Release Notes text, use the following best practices: -* Use present tense. +* Use present tense. * Use sentence case. * When you create a feature PR, start with `Adds`. -* When you create an enhancement PR, start with `Improves`. +* When you create an enhancement PR, start with `Improves`. * When you create a bug fix PR, start with `Fixes`. * When you create a deprecation PR, start with `Deprecates`. diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md index 5fa7d4841537b..48ec9456c56dd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md @@ -12,14 +12,14 @@ Serialize this format to a simple POJO, with only the params that are not defaul ```typescript toJSON(): { - id: unknown; - params: _.Dictionary | undefined; + id: any; + params: any; }; ``` Returns: `{ - id: unknown; - params: _.Dictionary | undefined; + id: any; + params: any; }` diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 928878fdcdb03..c83cd068eff5b 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -210,6 +210,25 @@ When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you c large exports from causing performance and storage issues. Defaults to `10485760` (10mB). +| `xpack.reporting.csv.scroll.size` + | Number of documents retrieved from {es} for each scroll iteration during a CSV + export. + Defaults to `500`. + +| `xpack.reporting.csv.scroll.duration` + | Amount of time allowed before {kib} cleans the scroll context during a CSV export. + Defaults to `30s`. + +| `xpack.reporting.csv.checkForFormulas` + | Enables a check that warns you when there's a potential formula involved in the output (=, -, +, and @ chars). + See OWASP: https://www.owasp.org/index.php/CSV_Injection + Defaults to `true`. + +| `xpack.reporting.csv.enablePanelActionDownload` + | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard + panel menu for the saved search. + Defaults to `true`. + |=== [float] diff --git a/package.json b/package.json index b1dd8686f818b..3c15d9ee3c97b 100644 --- a/package.json +++ b/package.json @@ -86,8 +86,9 @@ "**/@types/angular": "^1.6.56", "**/@types/hoist-non-react-statics": "^3.3.1", "**/@types/chai": "^4.2.11", + "**/cypress/@types/lodash": "^4.14.155", "**/typescript": "3.9.5", - "**/graphql-toolkit/lodash": "^4.17.13", + "**/graphql-toolkit/lodash": "^4.17.15", "**/hoist-non-react-statics": "^3.3.2", "**/isomorphic-git/**/base64-js": "^1.2.1", "**/image-diff/gm/debug": "^2.6.9", @@ -213,8 +214,7 @@ "leaflet.heat": "0.2.0", "less": "npm:@elastic/less@2.7.3-kibana", "less-loader": "5.0.0", - "lodash": "npm:@elastic/lodash@3.10.1-kibana4", - "lodash.clonedeep": "^4.5.0", + "lodash": "^4.17.15", "lru-cache": "4.1.5", "markdown-it": "^10.0.0", "mini-css-extract-plugin": "0.8.0", @@ -355,8 +355,7 @@ "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", - "@types/lodash": "^3.10.1", - "@types/lodash.clonedeep": "^4.5.4", + "@types/lodash": "^4.14.155", "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^0.0.7", "@types/minimatch": "^2.0.29", diff --git a/packages/kbn-config-schema/package.json b/packages/kbn-config-schema/package.json index 015dca128ce91..10b607dcd4312 100644 --- a/packages/kbn-config-schema/package.json +++ b/packages/kbn-config-schema/package.json @@ -14,6 +14,7 @@ "tsd": "^0.7.4" }, "peerDependencies": { + "lodash": "^4.17.15", "joi": "^13.5.2", "moment": "^2.24.0", "type-detect": "^4.0.8" diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index ea72a4a48caeb..c6bb06e68b9c0 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -11,8 +11,7 @@ "dependencies": { "@babel/runtime": "^7.10.2", "@kbn/i18n": "1.0.0", - "lodash": "npm:@elastic/lodash@3.10.1-kibana4", - "lodash.clone": "^4.5.0", + "lodash": "^4.17.15", "uuid": "3.3.2" }, "devDependencies": { diff --git a/packages/kbn-interpreter/src/common/lib/registry.js b/packages/kbn-interpreter/src/common/lib/registry.js index 25b122f400711..16572cf494cd3 100644 --- a/packages/kbn-interpreter/src/common/lib/registry.js +++ b/packages/kbn-interpreter/src/common/lib/registry.js @@ -17,7 +17,7 @@ * under the License. */ -import clone from 'lodash.clone'; +import { clone } from 'lodash'; export class Registry { constructor(prop = 'name') { diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md index 9ff0f56344274..5d5c5e3b6eb74 100644 --- a/packages/kbn-optimizer/README.md +++ b/packages/kbn-optimizer/README.md @@ -42,6 +42,26 @@ When a directory is listed in the "extraPublicDirs" it will always be included i Any import in a bundle which resolves into another bundles "context" directory, ie `src/plugins/*`, must map explicitly to a "public dir" exported by that plugin. If the resolved import is not in the list of public dirs an error will be thrown and the optimizer will fail to build that bundle until the error is fixed. +## Themes + +SASS imports in bundles are automatically converted to CSS for one or more themes. In development we build the `v7light` and `v7dark` themes by default to improve build performance. When producing distributable bundles the default shifts to `*` so that the distributable bundles will include all themes, preventing the bundles from needing to be rebuilt when users change the active theme in Kibana's advanced settings. + +To customize the themes that are built for development you can specify the `KBN_OPTIMIZER_THEMES` environment variable to one or more theme tags, or use `*` to build styles for all themes. Unfortunately building more than one theme significantly impacts build performance, so try to be strategic about which themes you build. + +Currently supported theme tags: `v7light`, `v7dark`, `v8light`, `v8dark` + +Examples: +```sh +# start Kibana with only a single theme +KBN_OPTIMIZER_THEMES=v7light yarn start + +# start Kibana with dark themes for version 7 and 8 +KBN_OPTIMIZER_THEMES=v7dark,v8dark yarn start + +# start Kibana with all the themes +KBN_OPTIMIZER_THEMES=* yarn start +``` + ## API To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][Optimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][Optimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used. diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss new file mode 100644 index 0000000000000..63beb9927b9f5 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss @@ -0,0 +1 @@ +$globalStyleConstant: 11; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8dark.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8dark.scss new file mode 100644 index 0000000000000..4040cab1878fc --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8dark.scss @@ -0,0 +1 @@ +$globalStyleConstant: 12; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8light.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8light.scss new file mode 100644 index 0000000000000..3918413c06863 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8light.scss @@ -0,0 +1 @@ +$globalStyleConstant: 13; diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index 7d021a5ee7847..89cde2c1cd064 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -29,3 +29,4 @@ export * from './array_helpers'; export * from './event_stream_helpers'; export * from './disallowed_syntax_plugin'; export * from './parse_path'; +export * from './theme_tags'; diff --git a/packages/kbn-optimizer/src/common/theme_tags.test.ts b/packages/kbn-optimizer/src/common/theme_tags.test.ts new file mode 100644 index 0000000000000..019a9b7bdee3e --- /dev/null +++ b/packages/kbn-optimizer/src/common/theme_tags.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { parseThemeTags } from './theme_tags'; + +it('returns default tags when passed undefined', () => { + expect(parseThemeTags()).toMatchInlineSnapshot(` + Array [ + "v7dark", + "v7light", + ] + `); +}); + +it('returns all tags when passed *', () => { + expect(parseThemeTags('*')).toMatchInlineSnapshot(` + Array [ + "v7dark", + "v7light", + "v8dark", + "v8light", + ] + `); +}); + +it('returns specific tag when passed a single value', () => { + expect(parseThemeTags('v8light')).toMatchInlineSnapshot(` + Array [ + "v8light", + ] + `); +}); + +it('returns specific tags when passed a comma separated list', () => { + expect(parseThemeTags('v8light, v7dark,v7light')).toMatchInlineSnapshot(` + Array [ + "v7dark", + "v7light", + "v8light", + ] + `); +}); + +it('returns specific tags when passed an array', () => { + expect(parseThemeTags(['v8light', 'v7light'])).toMatchInlineSnapshot(` + Array [ + "v7light", + "v8light", + ] + `); +}); + +it('throws when an invalid tag is in the array', () => { + expect(() => parseThemeTags(['v8light', 'v7light', 'bar'])).toThrowErrorMatchingInlineSnapshot( + `"Invalid theme tags [bar], options: [v7dark, v7light, v8dark, v8light]"` + ); +}); + +it('throws when an invalid tags in comma separated list', () => { + expect(() => parseThemeTags('v8light ,v7light,bar,box ')).toThrowErrorMatchingInlineSnapshot( + `"Invalid theme tags [bar, box], options: [v7dark, v7light, v8dark, v8light]"` + ); +}); + +it('returns tags in alphabetical order', () => { + const tags = parseThemeTags(['v7light', 'v8light']); + expect(tags).toEqual(tags.slice().sort((a, b) => a.localeCompare(b))); +}); + +it('returns an immutable array', () => { + expect(() => { + const tags = parseThemeTags('v8light'); + // @ts-expect-error + tags.push('foo'); + }).toThrowErrorMatchingInlineSnapshot(`"Cannot add property 1, object is not extensible"`); +}); diff --git a/packages/kbn-optimizer/src/common/theme_tags.ts b/packages/kbn-optimizer/src/common/theme_tags.ts new file mode 100644 index 0000000000000..27b5e12b807a8 --- /dev/null +++ b/packages/kbn-optimizer/src/common/theme_tags.ts @@ -0,0 +1,65 @@ +/* + * 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 { ascending } from './array_helpers'; + +const tags = (...themeTags: string[]) => + Object.freeze(themeTags.sort(ascending((tag) => tag)) as ThemeTag[]); + +const validTag = (tag: any): tag is ThemeTag => ALL_THEMES.includes(tag); +const isArrayOfStrings = (input: unknown): input is string[] => + Array.isArray(input) && input.every((v) => typeof v === 'string'); + +export type ThemeTags = readonly ThemeTag[]; +export type ThemeTag = 'v7light' | 'v7dark' | 'v8light' | 'v8dark'; +export const DEFAULT_THEMES = tags('v7light', 'v7dark'); +export const ALL_THEMES = tags('v7light', 'v7dark', 'v8light', 'v8dark'); + +export function parseThemeTags(input?: any): ThemeTags { + if (!input) { + return DEFAULT_THEMES; + } + + if (input === '*') { + return ALL_THEMES; + } + + if (typeof input === 'string') { + input = input.split(',').map((tag) => tag.trim()); + } + + if (!isArrayOfStrings(input)) { + throw new Error(`Invalid theme tags, must be an array of strings`); + } + + if (!input.length) { + throw new Error( + `Invalid theme tags, you must specify at least one of [${ALL_THEMES.join(', ')}]` + ); + } + + const invalidTags = input.filter((t) => !validTag(t)); + if (invalidTags.length) { + throw new Error( + `Invalid theme tags [${invalidTags.join(', ')}], options: [${ALL_THEMES.join(', ')}]` + ); + } + + return tags(...input); +} diff --git a/packages/kbn-optimizer/src/common/worker_config.ts b/packages/kbn-optimizer/src/common/worker_config.ts index a1ab51ee97c23..8726b3452ff1e 100644 --- a/packages/kbn-optimizer/src/common/worker_config.ts +++ b/packages/kbn-optimizer/src/common/worker_config.ts @@ -20,11 +20,13 @@ import Path from 'path'; import { UnknownVals } from './ts_helpers'; +import { ThemeTags, parseThemeTags } from './theme_tags'; export interface WorkerConfig { readonly repoRoot: string; readonly watch: boolean; readonly dist: boolean; + readonly themeTags: ThemeTags; readonly cache: boolean; readonly profileWebpack: boolean; readonly browserslistEnv: string; @@ -80,6 +82,8 @@ export function parseWorkerConfig(json: string): WorkerConfig { throw new Error('`browserslistEnv` must be a string'); } + const themes = parseThemeTags(parsed.themeTags); + return { repoRoot, cache, @@ -88,6 +92,7 @@ export function parseWorkerConfig(json: string): WorkerConfig { profileWebpack, optimizerCacheKey, browserslistEnv, + themeTags: themes, }; } catch (error) { throw new Error(`unable to parse worker config: ${error.message}`); diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index b6b0973f0d539..211cfac3806ad 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -58,11 +58,15 @@ OptimizerConfig { ], "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "themeTags": Array [ + "v7dark", + "v7light", + ], "watch": false, } `; -exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { tap((state) => { if (state.event?.type === 'worker stdio') { // eslint-disable-next-line no-console - console.log('worker', state.event.stream, state.event.chunk.toString('utf8')); + console.log('worker', state.event.stream, state.event.line); } }), toArray() @@ -226,7 +226,7 @@ const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabe // Verify the brotli variant matches expect( - // @ts-ignore @types/node is missing the brotli functions + // @ts-expect-error @types/node is missing the brotli functions Zlib.brotliDecompressSync( Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.br`)) ).toString() diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts index cbec159bd27a0..23767be610da4 100644 --- a/packages/kbn-optimizer/src/log_optimizer_state.ts +++ b/packages/kbn-optimizer/src/log_optimizer_state.ts @@ -24,7 +24,7 @@ import { tap } from 'rxjs/operators'; import { OptimizerConfig } from './optimizer'; import { OptimizerUpdate$ } from './run_optimizer'; -import { CompilerMsg, pipeClosure } from './common'; +import { CompilerMsg, pipeClosure, ALL_THEMES } from './common'; export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { return pipeClosure((update$: OptimizerUpdate$) => { @@ -37,12 +37,7 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { const { event, state } = update; if (event?.type === 'worker stdio') { - const chunk = event.chunk.toString('utf8'); - log.warning( - `worker`, - event.stream, - chunk.slice(0, chunk.length - (chunk.endsWith('\n') ? 1 : 0)) - ); + log.warning(`worker`, event.stream, event.line); } if (event?.type === 'bundle not cached') { @@ -76,6 +71,11 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { if (!loggedInit) { loggedInit = true; log.info(`initialized, ${state.offlineBundles.length} bundles cached`); + if (config.themeTags.length !== ALL_THEMES.length) { + log.warning( + `only building [${config.themeTags}] themes, customize with the KBN_OPTIMIZER_THEMES environment variable` + ); + } } return; } diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts index 9d7f1709506f9..47d01347a8f7d 100644 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -103,6 +103,10 @@ describe('getOptimizerCacheKey()', () => { "dist": false, "optimizerCacheKey": "♻", "repoRoot": , + "themeTags": Array [ + "v7dark", + "v7light", + ], }, } `); diff --git a/src/setup_node_env/harden.js b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts similarity index 54% rename from src/setup_node_env/harden.js rename to packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts index dead3db1d60b4..9bf8f9db1fe45 100644 --- a/src/setup_node_env/harden.js +++ b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts @@ -17,8 +17,33 @@ * under the License. */ -var hook = require('require-in-the-middle'); +import { Readable } from 'stream'; -hook(['child_process'], function (exports, name) { - return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require +import { toArray } from 'rxjs/operators'; + +import { observeStdio$ } from './observe_stdio'; + +it('notifies on every line, uncluding partial content at the end without a newline', async () => { + const chunks = [`foo\nba`, `r\nb`, `az`]; + + await expect( + observeStdio$( + new Readable({ + read() { + this.push(chunks.shift()!); + if (!chunks.length) { + this.push(null); + } + }, + }) + ) + .pipe(toArray()) + .toPromise() + ).resolves.toMatchInlineSnapshot(` + Array [ + "foo", + "bar", + "baz", + ] + `); }); diff --git a/packages/kbn-optimizer/src/optimizer/observe_stdio.ts b/packages/kbn-optimizer/src/optimizer/observe_stdio.ts new file mode 100644 index 0000000000000..e8daecef8e0dd --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/observe_stdio.ts @@ -0,0 +1,76 @@ +/* + * 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 { Readable } from 'stream'; +import * as Rx from 'rxjs'; + +// match newline characters followed either by a non-space character or another newline +const NEWLINE = /\r?\n/; + +/** + * Observe a readable stdio stream and emit the entire lines + * of text produced, completing once the stdio stream emits "end" + * and erroring if it emits "error". + */ +export function observeStdio$(stream: Readable) { + return new Rx.Observable((subscriber) => { + let buffer = ''; + + subscriber.add( + Rx.fromEvent(stream, 'data').subscribe({ + next(chunk) { + buffer += chunk.toString('utf8'); + + while (true) { + const match = NEWLINE.exec(buffer); + if (!match) { + break; + } + + const multilineChunk = buffer.slice(0, match.index); + buffer = buffer.slice(match.index + match[0].length); + subscriber.next(multilineChunk); + } + }, + }) + ); + + const flush = () => { + while (buffer.length && !subscriber.closed) { + const line = buffer; + buffer = ''; + subscriber.next(line); + } + }; + + subscriber.add( + Rx.fromEvent(stream, 'end').subscribe(() => { + flush(); + subscriber.complete(); + }) + ); + + subscriber.add( + Rx.fromEvent(stream, 'error').subscribe((error) => { + flush(); + subscriber.error(error); + }) + ); + }); +} diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts index fef3efc13a516..31b34bd5c5938 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -17,7 +17,6 @@ * under the License. */ -import { Readable } from 'stream'; import { inspect } from 'util'; import execa from 'execa'; @@ -26,12 +25,13 @@ import { map, takeUntil, first, ignoreElements } from 'rxjs/operators'; import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle, BundleRefs } from '../common'; +import { observeStdio$ } from './observe_stdio'; import { OptimizerConfig } from './optimizer_config'; export interface WorkerStdio { type: 'worker stdio'; stream: 'stdout' | 'stderr'; - chunk: Buffer; + line: string; } export interface WorkerStarted { @@ -99,28 +99,6 @@ function usingWorkerProc( ); } -function observeStdio$(stream: Readable, name: WorkerStdio['stream']) { - return Rx.fromEvent(stream, 'data').pipe( - takeUntil( - Rx.race( - Rx.fromEvent(stream, 'end'), - Rx.fromEvent(stream, 'error').pipe( - map((error) => { - throw error; - }) - ) - ) - ), - map( - (chunk): WorkerStdio => ({ - type: 'worker stdio', - chunk, - stream: name, - }) - ) - ); -} - /** * We used to pass configuration to the worker as JSON encoded arguments, but they * grew too large for argv, especially on Windows, so we had to move to an async init @@ -186,8 +164,24 @@ export function observeWorker( type: 'worker started', bundles, }), - observeStdio$(proc.stdout, 'stdout'), - observeStdio$(proc.stderr, 'stderr'), + observeStdio$(proc.stdout).pipe( + map( + (line): WorkerStdio => ({ + type: 'worker stdio', + line, + stream: 'stdout', + }) + ) + ), + observeStdio$(proc.stderr).pipe( + map( + (line): WorkerStdio => ({ + type: 'worker stdio', + line, + stream: 'stderr', + }) + ) + ), Rx.fromEvent<[unknown]>(proc, 'message') .pipe( // validate the messages from the process diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index d4152133f289d..5b46d67479fd5 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -20,6 +20,7 @@ jest.mock('./assign_bundles_to_workers.ts'); jest.mock('./kibana_platform_plugins.ts'); jest.mock('./get_plugin_bundles.ts'); +jest.mock('../common/theme_tags.ts'); import Path from 'path'; import Os from 'os'; @@ -27,6 +28,7 @@ import Os from 'os'; import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; import { OptimizerConfig } from './optimizer_config'; +import { parseThemeTags } from '../common'; jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any); @@ -35,6 +37,7 @@ expect.addSnapshotSerializer(createAbsolutePathSerializer()); beforeEach(() => { delete process.env.KBN_OPTIMIZER_MAX_WORKERS; delete process.env.KBN_OPTIMIZER_NO_CACHE; + delete process.env.KBN_OPTIMIZER_THEMES; jest.clearAllMocks(); }); @@ -81,6 +84,26 @@ describe('OptimizerConfig::parseOptions()', () => { }).toThrowErrorMatchingInlineSnapshot(`"worker count must be a number"`); }); + it('defaults to * theme when dist = true', () => { + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + dist: true, + }); + + expect(parseThemeTags).toBeCalledWith('*'); + }); + + it('defaults to KBN_OPTIMIZER_THEMES when dist = false', () => { + process.env.KBN_OPTIMIZER_THEMES = 'foo'; + + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + dist: false, + }); + + expect(parseThemeTags).toBeCalledWith('foo'); + }); + it('applies defaults', () => { expect( OptimizerConfig.parseOptions({ @@ -102,6 +125,7 @@ describe('OptimizerConfig::parseOptions()', () => { ], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -127,6 +151,7 @@ describe('OptimizerConfig::parseOptions()', () => { ], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -154,6 +179,7 @@ describe('OptimizerConfig::parseOptions()', () => { ], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -178,6 +204,7 @@ describe('OptimizerConfig::parseOptions()', () => { ], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -201,6 +228,7 @@ describe('OptimizerConfig::parseOptions()', () => { ], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -222,6 +250,7 @@ describe('OptimizerConfig::parseOptions()', () => { "pluginScanDirs": Array [], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -243,6 +272,7 @@ describe('OptimizerConfig::parseOptions()', () => { "pluginScanDirs": Array [], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -264,6 +294,7 @@ describe('OptimizerConfig::parseOptions()', () => { "pluginScanDirs": Array [], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -286,6 +317,7 @@ describe('OptimizerConfig::parseOptions()', () => { "pluginScanDirs": Array [], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -308,6 +340,7 @@ describe('OptimizerConfig::parseOptions()', () => { "pluginScanDirs": Array [], "profileWebpack": false, "repoRoot": , + "themeTags": undefined, "watch": false, } `); @@ -346,6 +379,7 @@ describe('OptimizerConfig::create()', () => { pluginScanDirs: Symbol('parsed plugin scan dirs'), repoRoot: Symbol('parsed repo root'), watch: Symbol('parsed watch'), + themeTags: Symbol('theme tags'), inspectWorkers: Symbol('parsed inspect workers'), profileWebpack: Symbol('parsed profile webpack'), })); @@ -369,6 +403,7 @@ describe('OptimizerConfig::create()', () => { "plugins": Symbol(new platform plugins), "profileWebpack": Symbol(parsed profile webpack), "repoRoot": Symbol(parsed repo root), + "themeTags": Symbol(theme tags), "watch": Symbol(parsed watch), } `); @@ -385,7 +420,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 7, + 21, ], "results": Array [ Object { @@ -408,7 +443,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 8, + 22, ], "results": Array [ Object { diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index c9e9b3ad01ccc..7757004139d0d 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -20,7 +20,14 @@ import Path from 'path'; import Os from 'os'; -import { Bundle, WorkerConfig, CacheableWorkerConfig } from '../common'; +import { + Bundle, + WorkerConfig, + CacheableWorkerConfig, + ThemeTag, + ThemeTags, + parseThemeTags, +} from '../common'; import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; @@ -73,6 +80,18 @@ interface Options { /** flag that causes the core bundle to be built along with plugins */ includeCoreBundle?: boolean; + + /** + * style themes that sass files will be converted to, the correct style will be + * loaded in the browser automatically by checking the global `__kbnThemeTag__`. + * Specifying additional styles increases build time. + * + * Defaults: + * - "*" when building the dist + * - comma separated list of themes in the `KBN_OPTIMIZER_THEMES` env var + * - "k7light" + */ + themes?: ThemeTag | '*' | ThemeTag[]; } interface ParsedOptions { @@ -86,6 +105,7 @@ interface ParsedOptions { pluginScanDirs: string[]; inspectWorkers: boolean; includeCoreBundle: boolean; + themeTags: ThemeTags; } export class OptimizerConfig { @@ -139,6 +159,10 @@ export class OptimizerConfig { throw new TypeError('worker count must be a number'); } + const themeTags = parseThemeTags( + options.themes || (dist ? '*' : process.env.KBN_OPTIMIZER_THEMES) + ); + return { watch, dist, @@ -150,6 +174,7 @@ export class OptimizerConfig { pluginPaths, inspectWorkers, includeCoreBundle, + themeTags, }; } @@ -181,7 +206,8 @@ export class OptimizerConfig { options.repoRoot, options.maxWorkerCount, options.dist, - options.profileWebpack + options.profileWebpack, + options.themeTags ); } @@ -194,7 +220,8 @@ export class OptimizerConfig { public readonly repoRoot: string, public readonly maxWorkerCount: number, public readonly dist: boolean, - public readonly profileWebpack: boolean + public readonly profileWebpack: boolean, + public readonly themeTags: ThemeTags ) {} getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { @@ -205,6 +232,7 @@ export class OptimizerConfig { repoRoot: this.repoRoot, watch: this.watch, optimizerCacheKey, + themeTags: this.themeTags, browserslistEnv: this.dist ? 'production' : process.env.BROWSERSLIST_ENV || 'dev', }; } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_state.ts b/packages/kbn-optimizer/src/optimizer/optimizer_state.ts index 1572f459e6ee5..09f8ca10c6181 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_state.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_state.ts @@ -127,7 +127,7 @@ export function createOptimizerStateSummarizer( } if (event.type === 'worker stdio' || event.type === 'worker started') { - // same state, but updated to the event is shared externally + // same state, but updated so the event is shared externally return createOptimizerState(state); } diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index de5e9372e9e7a..ca7673748bde9 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -77,7 +77,7 @@ const observeCompiler = ( */ const complete$ = Rx.fromEventPattern((cb) => done.tap(PLUGIN_NAME, cb)).pipe( maybeMap((stats) => { - // @ts-ignore not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58 + // @ts-expect-error not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58 if (stats.compilation.needAdditionalPass) { return undefined; } diff --git a/packages/kbn-optimizer/src/worker/theme_loader.ts b/packages/kbn-optimizer/src/worker/theme_loader.ts index 5d02462ef1bb8..f2f685bde65d9 100644 --- a/packages/kbn-optimizer/src/worker/theme_loader.ts +++ b/packages/kbn-optimizer/src/worker/theme_loader.ts @@ -17,16 +17,43 @@ * under the License. */ +import { stringifyRequest, getOptions } from 'loader-utils'; import webpack from 'webpack'; -import { stringifyRequest } from 'loader-utils'; +import { parseThemeTags, ALL_THEMES, ThemeTag } from '../common'; + +const getVersion = (tag: ThemeTag) => (tag.includes('v7') ? 7 : 8); +const getIsDark = (tag: ThemeTag) => tag.includes('dark'); +const compare = (a: ThemeTag, b: ThemeTag) => + (getVersion(a) === getVersion(b) ? 1 : 0) + (getIsDark(a) === getIsDark(b) ? 1 : 0); // eslint-disable-next-line import/no-default-export export default function (this: webpack.loader.LoaderContext) { + this.cacheable(true); + + const options = getOptions(this); + const bundleId: string = options.bundleId!; + const themeTags = parseThemeTags(options.themeTags); + + const cases = ALL_THEMES.map((tag) => { + if (themeTags.includes(tag)) { + return ` + case '${tag}': + return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});`; + } + + const fallback = themeTags + .slice() + .sort((a, b) => compare(b, tag) - compare(a, tag)) + .shift()!; + + const message = `SASS files in [${bundleId}] were not built for theme [${tag}]. Styles were compiled using the [${fallback}] theme instead to keep Kibana somewhat usable. Please adjust the advanced settings to make use of [${themeTags}] or make sure the KBN_OPTIMIZER_THEMES environment variable includes [${tag}] in a comma separated list of themes you want to compile. You can also set it to "*" to build all themes.`; + return ` + case '${tag}': + console.error(new Error(${JSON.stringify(message)})); + return require(${stringifyRequest(this, `${this.resourcePath}?${fallback}`)})`; + }).join('\n'); + return ` -if (window.__kbnDarkMode__) { - require(${stringifyRequest(this, `${this.resourcePath}?dark`)}) -} else { - require(${stringifyRequest(this, `${this.resourcePath}?light`)}); -} - `; +switch (window.__kbnThemeTag__) {${cases} +}`; } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 11f5544cd9274..aaea70d12c60d 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -21,11 +21,10 @@ import Path from 'path'; import { stringifyRequest } from 'loader-utils'; import webpack from 'webpack'; -// @ts-ignore +// @ts-expect-error import TerserPlugin from 'terser-webpack-plugin'; -// @ts-ignore +// @ts-expect-error import webpackMerge from 'webpack-merge'; -// @ts-ignore import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; @@ -134,8 +133,8 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: test: /\.scss$/, exclude: /node_modules/, oneOf: [ - { - resourceQuery: /dark|light/, + ...worker.themeTags.map((theme) => ({ + resourceQuery: `?${theme}`, use: [ { loader: 'style-loader', @@ -196,34 +195,27 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: loaderContext, Path.resolve( worker.repoRoot, - 'src/legacy/ui/public/styles/_styling_constants.scss' + `src/legacy/ui/public/styles/_globals_${theme}.scss` ) )};\n`; }, webpackImporter: false, implementation: require('node-sass'), - sassOptions(loaderContext: webpack.loader.LoaderContext) { - const darkMode = loaderContext.resourceQuery === '?dark'; - - return { - outputStyle: 'nested', - includePaths: [Path.resolve(worker.repoRoot, 'node_modules')], - sourceMapRoot: `/${bundle.type}:${bundle.id}`, - importer: (url: string) => { - if (darkMode && url.includes('eui_colors_light')) { - return { file: url.replace('eui_colors_light', 'eui_colors_dark') }; - } - - return { file: url }; - }, - }; + sassOptions: { + outputStyle: 'nested', + includePaths: [Path.resolve(worker.repoRoot, 'node_modules')], + sourceMapRoot: `/${bundle.type}:${bundle.id}`, }, }, }, ], - }, + })), { loader: require.resolve('./theme_loader'), + options: { + bundleId: bundle.id, + themeTags: worker.themeTags, + }, }, ], }, diff --git a/packages/kbn-plugin-generator/index.js b/packages/kbn-plugin-generator/index.js index e61037e42d63f..398b49fa1ecd5 100644 --- a/packages/kbn-plugin-generator/index.js +++ b/packages/kbn-plugin-generator/index.js @@ -23,7 +23,7 @@ const dedent = require('dedent'); const sao = require('sao'); const chalk = require('chalk'); const getopts = require('getopts'); -const snakeCase = require('lodash.snakecase'); +const { snakeCase } = require('lodash'); exports.run = function run(argv) { const options = getopts(argv, { @@ -41,7 +41,7 @@ exports.run = function run(argv) { if (options.help) { console.log( dedent(chalk` - # {dim Usage:} + # {dim Usage:} node scripts/generate-plugin {bold [name]} Generate a fresh Kibana plugin in the plugins/ directory `) + '\n' diff --git a/packages/kbn-plugin-generator/package.json b/packages/kbn-plugin-generator/package.json index b9df67b32e5d3..5c1e98cd869de 100644 --- a/packages/kbn-plugin-generator/package.json +++ b/packages/kbn-plugin-generator/package.json @@ -8,10 +8,7 @@ "dedent": "^0.7.0", "execa": "^4.0.2", "getopts": "^2.2.4", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", + "lodash": "^4.17.15", "sao": "^0.22.12" } } diff --git a/packages/kbn-plugin-generator/sao_template/sao.js b/packages/kbn-plugin-generator/sao_template/sao.js index 7fc29b1e6bd0a..dc4d8a2fc10fb 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.js +++ b/packages/kbn-plugin-generator/sao_template/sao.js @@ -20,9 +20,7 @@ const { relative, resolve } = require('path'); const fs = require('fs'); -const startCase = require('lodash.startcase'); -const camelCase = require('lodash.camelcase'); -const snakeCase = require('lodash.snakecase'); +const { camelCase, startCase, snakeCase } = require('lodash'); const chalk = require('chalk'); const execa = require('execa'); diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 3e7ed49c61314..188db0a8321a2 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -22,7 +22,7 @@ "@types/glob": "^5.0.35", "@types/globby": "^6.1.0", "@types/has-ansi": "^3.0.0", - "@types/lodash.clonedeepwith": "^4.5.3", + "@types/lodash": "^4.14.155", "@types/log-symbols": "^2.0.0", "@types/ncp": "^2.0.1", "@types/node": ">=10.17.17 <10.20.0", @@ -46,7 +46,7 @@ "globby": "^8.0.1", "has-ansi": "^3.0.0", "is-path-inside": "^3.0.2", - "lodash.clonedeepwith": "^4.5.0", + "lodash": "^4.17.15", "log-symbols": "^2.2.0", "multimatch": "^4.0.0", "ncp": "^2.0.0", diff --git a/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts b/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts index 96ce6fd1d919a..cf4ecbb4ad42c 100644 --- a/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts +++ b/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts @@ -17,7 +17,7 @@ * under the License. */ -import cloneDeepWith from 'lodash.clonedeepwith'; +import { cloneDeepWith } from 'lodash'; import { resolve, sep as pathSep } from 'path'; const repoRoot = resolve(__dirname, '../../../../'); diff --git a/packages/kbn-storybook/lib/webpack.dll.config.js b/packages/kbn-storybook/lib/webpack.dll.config.js index 534f503e2956a..740ee3819c36f 100644 --- a/packages/kbn-storybook/lib/webpack.dll.config.js +++ b/packages/kbn-storybook/lib/webpack.dll.config.js @@ -54,7 +54,6 @@ module.exports = { 'highlight.js', 'html-entities', 'jquery', - 'lodash.clone', 'lodash', 'markdown-it', 'mocha', diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index caeffaabea62b..b2df4f40d4fbe 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -122,7 +122,7 @@ module.exports = async ({ config }) => { prependData(loaderContext) { return `@import ${stringifyRequest( loaderContext, - resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') )};\n`; }, sassOptions: { diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 042de2617565e..0c49ccf276b2b 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -14,6 +14,7 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@types/joi": "^13.4.2", + "@types/lodash": "^4.14.155", "@types/parse-link-header": "^1.0.0", "@types/puppeteer": "^3.0.0", "@types/strip-ansi": "^5.2.1", @@ -28,6 +29,7 @@ "getopts": "^2.2.4", "glob": "^7.1.2", "joi": "^13.5.2", + "lodash": "^4.17.15", "parse-link-header": "^1.0.1", "puppeteer": "^3.3.0", "rxjs": "^6.5.5", diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts index e38520f00e45b..687a0e87d4c68 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts @@ -18,10 +18,7 @@ */ import { Schema } from 'joi'; -import { cloneDeep, get, has } from 'lodash'; - -// @ts-ignore internal lodash module is not typed -import toPath from 'lodash/internal/toPath'; +import { cloneDeepWith, get, has, toPath } from 'lodash'; import { schema } from './schema'; @@ -114,7 +111,7 @@ export class Config { throw new Error(`Unknown config key "${key}"`); } - return cloneDeep(get(this[$values], key, defaultValue), (v) => { + return cloneDeepWith(get(this[$values], key, defaultValue), (v) => { if (typeof v === 'function') { return v; } @@ -122,7 +119,7 @@ export class Config { } public getAll() { - return cloneDeep(this[$values], (v) => { + return cloneDeepWith(this[$values], (v) => { if (typeof v === 'function') { return v; } diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index f795b32d78b8e..2d4c461cc2c2e 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -19,8 +19,7 @@ import { resolve } from 'path'; import { format } from 'url'; -import { get } from 'lodash'; -import toPath from 'lodash/internal/toPath'; +import { get, toPath } from 'lodash'; import { Cluster } from '@kbn/es'; import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; import { esTestConfig } from './es_test_config'; diff --git a/packages/kbn-test/src/page_load_metrics/navigation.ts b/packages/kbn-test/src/page_load_metrics/navigation.ts index 21dc681951b21..db53df789ac69 100644 --- a/packages/kbn-test/src/page_load_metrics/navigation.ts +++ b/packages/kbn-test/src/page_load_metrics/navigation.ts @@ -19,7 +19,6 @@ import Fs from 'fs'; import Url from 'url'; -import _ from 'lodash'; import puppeteer from 'puppeteer'; import { resolve } from 'path'; import { ToolingLog } from '@kbn/dev-utils'; diff --git a/packages/kbn-ui-framework/Gruntfile.js b/packages/kbn-ui-framework/Gruntfile.js index 177fd1f153155..b7ba1e87b2f00 100644 --- a/packages/kbn-ui-framework/Gruntfile.js +++ b/packages/kbn-ui-framework/Gruntfile.js @@ -21,7 +21,7 @@ const sass = require('node-sass'); const postcss = require('postcss'); const postcssConfig = require('../../src/optimize/postcss.config'); const chokidar = require('chokidar'); -const debounce = require('lodash/function/debounce'); +const { debounce } = require('lodash'); const platform = require('os').platform(); const isPlatformWindows = /^win/.test(platform); diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 4da4fb21fbed5..abf64906e0253 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -17,7 +17,7 @@ "dependencies": { "classnames": "2.2.6", "focus-trap-react": "^3.1.1", - "lodash": "npm:@elastic/lodash@3.10.1-kibana4", + "lodash": "^4.17.15", "prop-types": "15.6.0", "react": "^16.12.0", "react-ace": "^5.9.0", diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 02b64157686c1..0f981f3d07610 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -51,15 +51,6 @@ export const ElasticEui = require('@elastic/eui'); export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format'); export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme'); -export let ElasticEuiLightTheme; -export let ElasticEuiDarkTheme; -if (window.__kbnThemeVersion__ === 'v7') { - ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json'); - ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json'); -} else { - ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_amsterdam_light.json'); - ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_amsterdam_dark.json'); -} import * as Theme from './theme.ts'; export { Theme }; diff --git a/packages/kbn-ui-shared-deps/theme.ts b/packages/kbn-ui-shared-deps/theme.ts index ca4714779d39e..4b2758516fc26 100644 --- a/packages/kbn-ui-shared-deps/theme.ts +++ b/packages/kbn-ui-shared-deps/theme.ts @@ -23,9 +23,15 @@ const globals: any = typeof window === 'undefined' ? {} : window; export type Theme = typeof LightTheme; +// in the Kibana app we can rely on this global being defined, but in +// some cases (like jest, or karma tests) the global is undefined +export const tag: string = globals.__kbnThemeTag__ || 'v7light'; +export const version = tag.startsWith('v7') ? 7 : 8; +export const darkMode = tag.endsWith('dark'); + export let euiLightVars: Theme; export let euiDarkVars: Theme; -if (globals.__kbnThemeVersion__ === 'v7') { +if (version === 7) { euiLightVars = require('@elastic/eui/dist/eui_theme_light.json'); euiDarkVars = require('@elastic/eui/dist/eui_theme_dark.json'); } else { @@ -37,7 +43,7 @@ if (globals.__kbnThemeVersion__ === 'v7') { * EUI Theme vars that automatically adjust to light/dark theme */ export let euiThemeVars: Theme; -if (globals.__kbnDarkTheme__) { +if (darkMode) { euiThemeVars = euiDarkVars; } else { euiThemeVars = euiLightVars; diff --git a/renovate.json5 b/renovate.json5 index 49a255d60f29e..5a807b4b090c1 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -557,22 +557,6 @@ '@types/lodash', ], }, - { - groupSlug: 'lodash.clonedeep', - groupName: 'lodash.clonedeep related packages', - packageNames: [ - 'lodash.clonedeep', - '@types/lodash.clonedeep', - ], - }, - { - groupSlug: 'lodash.clonedeepwith', - groupName: 'lodash.clonedeepwith related packages', - packageNames: [ - 'lodash.clonedeepwith', - '@types/lodash.clonedeepwith', - ], - }, { groupSlug: 'log-symbols', groupName: 'log-symbols related packages', diff --git a/src/apm.js b/src/apm.js index 6c10539c6b7d3..effa6c77d7614 100644 --- a/src/apm.js +++ b/src/apm.js @@ -20,7 +20,7 @@ const { join } = require('path'); const { readFileSync } = require('fs'); const { execSync } = require('child_process'); -const merge = require('lodash.merge'); +const { merge } = require('lodash'); const { name, version, build } = require('../package.json'); const ROOT_DIR = join(__dirname, '..'); diff --git a/src/cli/cluster/cluster_manager.test.ts b/src/cli/cluster/cluster_manager.test.ts index 66f68f815edac..2ddccae2fada6 100644 --- a/src/cli/cluster/cluster_manager.test.ts +++ b/src/cli/cluster/cluster_manager.test.ts @@ -93,7 +93,7 @@ describe('CLI cluster manager', () => { } const football = {}; - const messenger = sample(manager.workers); + const messenger = sample(manager.workers) as any; messenger.emit('broadcast', football); for (const worker of manager.workers) { diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts index dc6e6d5676651..097a549187429 100644 --- a/src/cli/cluster/worker.ts +++ b/src/cli/cluster/worker.ts @@ -177,7 +177,7 @@ export class Worker extends EventEmitter { } flushChangeBuffer() { - const files = _.unique(this.changes.splice(0)); + const files = _.uniq(this.changes.splice(0)); const prefix = files.length > 1 ? '\n - ' : ''; return files.reduce(function (list, file) { return `${list || ''}${prefix}"${file}"`; diff --git a/src/cli/help.js b/src/cli/help.js index 656944d85b254..0170cb53e19df 100644 --- a/src/cli/help.js +++ b/src/cli/help.js @@ -72,7 +72,7 @@ function commandsSummary(program) { }, 0); return cmds.reduce(function (help, cmd) { - return `${help || ''}${_.padRight(cmd[0], cmdLColWidth)} ${cmd[1] || ''}\n`; + return `${help || ''}${_.padEnd(cmd[0], cmdLColWidth)} ${cmd[1] || ''}\n`; }, ''); } diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index bf9b4235e9444..e31094d96f3d4 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -17,7 +17,7 @@ * under the License. */ -import { merge } from 'lodash'; +import { omitBy } from 'lodash'; import { format } from 'url'; import { BehaviorSubject } from 'rxjs'; @@ -42,6 +42,10 @@ interface Params { const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; +const removedUndefined = (obj: Record | undefined) => { + return omitBy(obj, (v) => v === undefined); +}; + export class Fetch { private readonly interceptors = new Set(); private readonly requestCount$ = new BehaviorSubject(0); @@ -119,24 +123,23 @@ export class Fetch { asResponse, asSystemRequest, ...fetchOptions - } = merge( - { - method: 'GET', - credentials: 'same-origin', - prependBasePath: true, - }, - options, - { - headers: { - 'Content-Type': 'application/json', - ...options.headers, - 'kbn-version': this.params.kibanaVersion, - }, - } - ); + } = { + method: 'GET', + credentials: 'same-origin', + prependBasePath: true, + ...options, + // options can pass an `undefined` Content-Type to erase the default value. + // however we can't pass it to `fetch` as it will send an `Content-Type: Undefined` header + headers: removedUndefined({ + 'Content-Type': 'application/json', + ...options.headers, + 'kbn-version': this.params.kibanaVersion, + }), + }; + const url = format({ pathname: shouldPrependBasePath ? this.params.basePath.prepend(options.path) : options.path, - query, + query: removedUndefined(query), }); // Make sure the system request header is only present if `asSystemRequest` is true. @@ -144,7 +147,7 @@ export class Fetch { fetchOptions.headers['kbn-system-request'] = 'true'; } - return new Request(url, fetchOptions); + return new Request(url, fetchOptions as RequestInit); } private async fetchResponse(fetchOptions: HttpFetchOptionsWithPath): Promise> { diff --git a/src/core/public/index.scss b/src/core/public/index.scss index 4be46899cff67..87825350b4e98 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -1,7 +1,3 @@ -// This file is built by both the legacy and KP build systems so we need to -// import this explicitly -@import '../../legacy/ui/public/styles/_styling_constants'; - @import './core'; @import './chrome/index'; @import './overlays/index'; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index cb8671ba37a6c..7dc5f3655fca0 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -91,7 +91,7 @@ describe('PluginsService', () => { context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), - injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), + injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), }; @@ -99,6 +99,7 @@ describe('PluginsService', () => { ...mockSetupDeps, application: expect.any(Object), getStartServices: expect.any(Function), + injectedMetadata: pick(mockSetupDeps.injectedMetadata, 'getInjectedVar'), }; mockStartDeps = { application: applicationServiceMock.createInternalStartContract(), @@ -106,7 +107,7 @@ describe('PluginsService', () => { http: httpServiceMock.createStartContract(), chrome: chromeServiceMock.createStartContract(), i18n: i18nServiceMock.createStartContract(), - injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), + injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), @@ -117,6 +118,7 @@ describe('PluginsService', () => { ...mockStartDeps, application: expect.any(Object), chrome: omit(mockStartDeps.chrome, 'getComponent'), + injectedMetadata: pick(mockStartDeps.injectedMetadata, 'getInjectedVar'), }; // Reset these for each test. diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index cb279b2cc4c8f..c4daaf5d7f307 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -162,7 +162,9 @@ export class SavedObjectsClient { }); if (!foundObject) { - return queueItem.resolve(this.createSavedObject(pick(queueItem, ['id', 'type']))); + return queueItem.resolve( + this.createSavedObject(pick(queueItem, ['id', 'type']) as SavedObject) + ); } queueItem.resolve(foundObject); diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index d3ba506b865a4..165ef98be91d4 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -60,7 +60,7 @@ export class SimpleSavedObject { } public set(key: string, value: any): T { - return set(this.attributes, key, value); + return set(this.attributes as any, key, value); } public has(key: string): boolean { diff --git a/src/core/server/capabilities/merge_capabilities.ts b/src/core/server/capabilities/merge_capabilities.ts index 95296346ad835..06869089598a9 100644 --- a/src/core/server/capabilities/merge_capabilities.ts +++ b/src/core/server/capabilities/merge_capabilities.ts @@ -17,11 +17,11 @@ * under the License. */ -import { merge } from 'lodash'; +import { mergeWith } from 'lodash'; import { Capabilities } from './types'; export const mergeCapabilities = (...sources: Array>): Capabilities => - merge({}, ...sources, (a: any, b: any) => { + mergeWith({}, ...sources, (a: any, b: any) => { if ( (typeof a === 'boolean' && typeof b === 'object') || (typeof a === 'object' && typeof b === 'boolean') diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 483534e0c145b..715f5b883139f 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -39,10 +39,7 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { }; const xsrfDeprecation: ConfigDeprecation = (settings, fromPath, log) => { - if ( - has(settings, 'server.xsrf.whitelist') && - get(settings, 'server.xsrf.whitelist').length > 0 - ) { + if ((settings.server?.xsrf?.whitelist ?? []).length > 0) { log( 'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' + 'It will be removed in 8.0 release. Instead, supply the "kbn-xsrf" header.' diff --git a/src/core/server/elasticsearch/legacy/errors.ts b/src/core/server/elasticsearch/legacy/errors.ts index f81903d76547a..3b3b8da51a907 100644 --- a/src/core/server/elasticsearch/legacy/errors.ts +++ b/src/core/server/elasticsearch/legacy/errors.ts @@ -81,7 +81,7 @@ export class LegacyElasticsearchErrorHelpers { public static decorateNotAuthorizedError(error: Error, reason?: string) { const decoratedError = decorate(error, ErrorCode.NOT_AUTHORIZED, 401, reason); - const wwwAuthHeader = get(error, 'body.error.header[WWW-Authenticate]'); + const wwwAuthHeader = get(error, 'body.error.header[WWW-Authenticate]') as string; decoratedError.output.headers['WWW-Authenticate'] = wwwAuthHeader || 'Basic realm="Authorization Required"'; diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index ffbdabadd03f7..eccc9d013176c 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -24,7 +24,7 @@ import apm from 'elastic-apm-node'; import { ByteSizeValue } from '@kbn/config-schema'; import { Server, Request, ResponseToolkit } from 'hapi'; import HapiProxy from 'h2o2'; -import { sample } from 'lodash'; +import { sampleSize } from 'lodash'; import BrowserslistUserAgent from 'browserslist-useragent'; import * as Rx from 'rxjs'; import { take } from 'rxjs/operators'; @@ -90,7 +90,7 @@ export class BasePathProxyServer { httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE); if (!httpConfig.basePath) { - httpConfig.basePath = `/${sample(alphabet, 3).join('')}`; + httpConfig.basePath = `/${sampleSize(alphabet, 3).join('')}`; } } diff --git a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 3b16bed92df97..4a6d86a0dfba6 100644 --- a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -119,7 +119,10 @@ Object { exports[`#set correctly sets values for paths that do not exist. 1`] = ` Object { - "unknown": "value", + "unknown": Object { + "sub1": "sub-value-1", + "sub2": "sub-value-2", + }, } `; diff --git a/src/core/server/saved_objects/mappings/lib/get_property.ts b/src/core/server/saved_objects/mappings/lib/get_property.ts index a31c9fe0c3ba1..91b2b1239fc55 100644 --- a/src/core/server/saved_objects/mappings/lib/get_property.ts +++ b/src/core/server/saved_objects/mappings/lib/get_property.ts @@ -17,7 +17,7 @@ * under the License. */ -import toPath from 'lodash/internal/toPath'; +import { toPath } from 'lodash'; import { SavedObjectsCoreFieldMapping, SavedObjectsFieldMapping, IndexMapping } from '../types'; function getPropertyMappingFromObjectMapping( diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 376f823267ebe..07675bb0a6819 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -62,7 +62,6 @@ import Boom from 'boom'; import _ from 'lodash'; -import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; import { Logger } from '../../../logging'; import { SavedObjectUnsanitizedDoc } from '../../serialization'; @@ -151,7 +150,7 @@ export class DocumentMigrator implements VersionedTransformer { // Clone the document to prevent accidental mutations on the original data // Ex: Importing sample data that is cached at import level, migrations would // execute on mutated data the second time. - const clonedDoc = cloneDeep(doc); + const clonedDoc = _.cloneDeep(doc); return this.transformDoc(clonedDoc); }; } @@ -220,7 +219,7 @@ function buildActiveMigrations( return { ...migrations, [type.name]: { - latestVersion: _.last(transforms).version, + latestVersion: _.last(transforms)!.version, transforms, }, }; diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts index 3f2c31a7c0e5c..2d27ca7c8a29b 100644 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import _ from 'lodash'; import { coordinateMigration } from './migration_coordinator'; import { createSavedObjectsMigrationLoggerMock } from '../mocks'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index f24195c0f295e..880b71e164b5b 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1346,7 +1346,7 @@ export class SavedObjectsRepository { // method transparently to the specified namespace. private _rawToSavedObject(raw: SavedObjectsRawDoc): SavedObject { const savedObject = this._serializer.rawToSavedObject(raw); - return omit(savedObject, 'namespace'); + return omit(savedObject, 'namespace') as SavedObject; } /** diff --git a/src/core/utils/deep_freeze.test.ts b/src/core/utils/deep_freeze.test.ts index 58aa9c9b8c92b..48f890160d05d 100644 --- a/src/core/utils/deep_freeze.test.ts +++ b/src/core/utils/deep_freeze.test.ts @@ -32,7 +32,8 @@ it('returns the first argument with all original references', () => { it('prevents adding properties to argument', () => { const frozen = deepFreeze({}); expect(() => { - // @ts-expect-error ts knows this shouldn't be possible, but just making sure + // ts knows this shouldn't be possible, but just making sure + // @ts-expect-error frozen.foo = true; }).toThrowError(`object is not extensible`); }); @@ -40,7 +41,8 @@ it('prevents adding properties to argument', () => { it('prevents changing properties on argument', () => { const frozen = deepFreeze({ foo: false }); expect(() => { - // @ts-expect-error ts knows this shouldn't be possible, but just making sure + // ts knows this shouldn't be possible, but just making sure + // @ts-expect-error frozen.foo = true; }).toThrowError(`read only property 'foo'`); }); @@ -48,7 +50,8 @@ it('prevents changing properties on argument', () => { it('prevents changing properties on nested children of argument', () => { const frozen = deepFreeze({ foo: { bar: { baz: { box: 1 } } } }); expect(() => { - // @ts-expect-error ts knows this shouldn't be possible, but just making sure + // ts knows this shouldn't be possible, but just making sure + // @ts-expect-error frozen.foo.bar.baz.box = 2; }).toThrowError(`read only property 'box'`); }); @@ -56,7 +59,8 @@ it('prevents changing properties on nested children of argument', () => { it('prevents adding items to a frozen array', () => { const frozen = deepFreeze({ foo: [1] }); expect(() => { - // @ts-expect-error ts knows this shouldn't be possible, but just making sure + // ts knows this shouldn't be possible, but just making sure + // @ts-expect-error frozen.foo.push(2); }).toThrowError(`object is not extensible`); }); @@ -64,7 +68,8 @@ it('prevents adding items to a frozen array', () => { it('prevents reassigning items in a frozen array', () => { const frozen = deepFreeze({ foo: [1] }); expect(() => { - // @ts-expect-error ts knows this shouldn't be possible, but just making sure + // ts knows this shouldn't be possible, but just making sure + // @ts-expect-error frozen.foo[0] = 2; }).toThrowError(`read only property '0'`); }); diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index bc3a6265cc582..cec80dd547a53 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -98,7 +98,6 @@ export const IGNORE_DIRECTORY_GLOBS = [ 'packages/*', 'packages/kbn-ui-framework/generator-kui', 'src/legacy/ui/public/flot-charts', - 'src/legacy/ui/public/utils/lodash-mixins', 'test/functional/fixtures/es_archiver/visualize_source-filters', 'packages/kbn-pm/src/utils/__fixtures__/*', 'x-pack/dev-tools', diff --git a/src/dev/sass/build_sass.js b/src/dev/sass/build_sass.js index 7075bcf55adf5..68058043477d0 100644 --- a/src/dev/sass/build_sass.js +++ b/src/dev/sass/build_sass.js @@ -23,11 +23,11 @@ import * as Rx from 'rxjs'; import { toArray } from 'rxjs/operators'; import { createFailError } from '@kbn/dev-utils'; +import { debounce } from 'lodash'; import { findPluginSpecs } from '../../legacy/plugin_discovery'; import { collectUiExports } from '../../legacy/ui'; import { buildAll } from '../../legacy/server/sass/build_all'; import chokidar from 'chokidar'; -import debounce from 'lodash/function/debounce'; // TODO: clintandrewhall - Extract and use FSWatcher from legacy/server/sass const build = async ({ log, kibanaDir, styleSheetPaths, watch }) => { diff --git a/src/fixtures/agg_resp/geohash_grid.js b/src/fixtures/agg_resp/geohash_grid.js index 0e576a88ab36a..fde1e54b0661d 100644 --- a/src/fixtures/agg_resp/geohash_grid.js +++ b/src/fixtures/agg_resp/geohash_grid.js @@ -44,7 +44,7 @@ export default function GeoHashGridAggResponseFixture() { // random number of tags let docCount = 0; const buckets = _.times(_.random(40, 200), function () { - return _.sample(geoHashCharts, 3).join(''); + return _.sampleSize(geoHashCharts, 3).join(''); }) .sort() .map(function (geoHash) { diff --git a/src/legacy/core_plugins/console_legacy/index.ts b/src/legacy/core_plugins/console_legacy/index.ts index c588b941112d1..82e00a99c6cfd 100644 --- a/src/legacy/core_plugins/console_legacy/index.ts +++ b/src/legacy/core_plugins/console_legacy/index.ts @@ -41,7 +41,7 @@ export default function (kibana: any) { uiExports: { injectDefaultVars: () => ({ elasticsearchUrl: url.format( - Object.assign(url.parse(head(_legacyEsConfig.hosts)), { auth: false }) + Object.assign(url.parse(head(_legacyEsConfig.hosts) as any), { auth: false }) ), }), }, diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js b/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js index fc4ff512e2bd5..d76b2a2aa9364 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js +++ b/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js @@ -35,7 +35,7 @@ export function handleESError(error) { return Boom.serverUnavailable(error); } else if ( error instanceof esErrors.Conflict || - _.contains(error.message, 'index_template_already_exists') + _.includes(error.message, 'index_template_already_exists') ) { return Boom.conflict(error); } else if (error instanceof esErrors[403]) { diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js index 4f8cee2651a9f..20281d8479ab4 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js @@ -51,7 +51,7 @@ describe('Vislib Dispatch Class Test Suite', function () { }); it('implements on, off, emit methods', function () { - const events = _.pluck(vis.handler.charts, 'events'); + const events = _.map(vis.handler.charts, 'events'); expect(events.length).to.be.above(0); events.forEach(function (dispatch) { expect(dispatch).to.have.property('on'); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js index f075dff466793..6b7ccaed25d49 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js @@ -267,7 +267,7 @@ describe('stackData method - data set with zeros in percentage mode', function ( expect(chart.chartData.series).to.have.length(1); const series = chart.chartData.series[0].values; // with the interval set in seriesMonthlyInterval data, the point at x=1454309600000 does not exist - const point = _.find(series, 'x', 1454309600000); + const point = _.find(series, ['x', 1454309600000]); expect(point).to.not.be(undefined); expect(point.y).to.be(0); }); @@ -279,7 +279,7 @@ describe('stackData method - data set with zeros in percentage mode', function ( const chart = vis.handler.charts[0]; expect(chart.chartData.series).to.have.length(5); const series = chart.chartData.series[0].values; - const point = _.find(series, 'x', 1415826240000); + const point = _.find(series, ['x', 1415826240000]); expect(point).to.not.be(undefined); expect(point.y).to.be(0); }); diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss index e9810a747c8c7..7de0c8fc15f94 100644 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ b/src/legacy/core_plugins/kibana/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Elastic charts @import '@elastic/charts/dist/theme'; @import '@elastic/eui/src/themes/charts/theme'; diff --git a/src/legacy/core_plugins/tests_bundle/public/index.scss b/src/legacy/core_plugins/tests_bundle/public/index.scss index 8020cef8d8492..d8dbf8d6dc885 100644 --- a/src/legacy/core_plugins/tests_bundle/public/index.scss +++ b/src/legacy/core_plugins/tests_bundle/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // This file pulls some styles of NP plugins into the legacy test stylesheet // so they are available for karma browser tests. @import '../../../../plugins/vis_type_vislib/public/index'; diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index b5501982cec09..602b221b7d14d 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -427,7 +427,7 @@ app.controller('timelion', function ( const httpResult = $http .post('../api/timelion/run', { sheet: $scope.state.sheet, - time: _.extend( + time: _.assignIn( { from: timeRangeBounds.min, to: timeRangeBounds.max, diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js index 879fab206b99d..ae042310fd464 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js @@ -165,7 +165,7 @@ module }; self.getLabel = function () { - return _.words(self.properties.nouns).map(_.capitalize).join(' '); + return _.words(self.properties.nouns).map(_.upperFirst).join(' '); }; //key handler for the filter text box diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js index f3fd2fde8f2c5..2102b02194bc8 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js @@ -78,7 +78,7 @@ export function TimelionExpInput($http, $timeout) { function init() { $http.get('../api/timelion/functions').then(function (resp) { Object.assign(functionReference, { - byName: _.indexBy(resp.data, 'name'), + byName: _.keyBy(resp.data, 'name'), list: resp.data, }); }); diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js b/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js index 577ee984e05c6..3750e15c000e7 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js @@ -47,7 +47,7 @@ export function TimelionInterval($timeout) { // Only run this on initialization if (newVal !== oldVal || oldVal == null) return; - if (_.contains($scope.intervalOptions, newVal)) { + if (_.includes($scope.intervalOptions, newVal)) { $scope.interval = newVal; } else { $scope.interval = 'other'; diff --git a/src/legacy/core_plugins/timelion/public/index.scss b/src/legacy/core_plugins/timelion/public/index.scss index ebf000d160b54..cf2a7859a505d 100644 --- a/src/legacy/core_plugins/timelion/public/index.scss +++ b/src/legacy/core_plugins/timelion/public/index.scss @@ -1,6 +1,3 @@ -// Should import both the EUI constants and any Kibana ones that are considered global -@import 'src/legacy/ui/public/styles/styling_constants'; - /* Timelion plugin styles */ // Prefix all styles with "tim" to avoid conflicts. diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts index b1999eb4b483c..087e166925327 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts +++ b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts @@ -346,7 +346,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) { } if (serie._global) { - _.merge(options, serie._global, function (objVal, srcVal) { + _.mergeWith(options, serie._global, function (objVal, srcVal) { // This is kind of gross, it means that you can't replace a global value with a null // best you can do is an empty string. Deal with it. if (objVal == null) return srcVal; diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js index 17da5ffca1242..db1ec425f2ce5 100644 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js @@ -19,8 +19,7 @@ import { resolve, basename, isAbsolute as isAbsolutePath } from 'path'; -import toPath from 'lodash/internal/toPath'; -import { get } from 'lodash'; +import { get, toPath } from 'lodash'; import { createInvalidPluginError } from '../errors'; import { isVersionCompatible } from './is_version_compatible'; diff --git a/src/legacy/server/i18n/localization/file_integrity.ts b/src/legacy/server/i18n/localization/file_integrity.ts index a852fba4a1c5a..7400d84ea2ce7 100644 --- a/src/legacy/server/i18n/localization/file_integrity.ts +++ b/src/legacy/server/i18n/localization/file_integrity.ts @@ -33,7 +33,7 @@ export interface Integrities { export async function getIntegrityHashes(filepaths: string[]): Promise { const hashes = await Promise.all(filepaths.map(getIntegrityHash)); - return zipObject(filepaths, hashes); + return zipObject(filepaths, hashes) as Integrities; } export async function getIntegrityHash(filepath: string): Promise { diff --git a/src/legacy/server/logging/log_format.js b/src/legacy/server/logging/log_format.js index 9bc1d67dd5857..8a80cbef1a9c5 100644 --- a/src/legacy/server/logging/log_format.js +++ b/src/legacy/server/logging/log_format.js @@ -144,7 +144,7 @@ export default class TransformObjStream extends Stream.Transform { data.message = message || 'Unknown error (no message)'; } else if (event.error instanceof Error) { data.type = 'error'; - data.level = _.contains(event.tags, 'fatal') ? 'fatal' : 'error'; + data.level = _.includes(event.tags, 'fatal') ? 'fatal' : 'error'; data.error = serializeError(event.error); const message = get(event, 'error.message'); data.message = message || 'Unknown error object (no message)'; diff --git a/src/legacy/server/sass/__fixtures__/index.scss b/src/legacy/server/sass/__fixtures__/index.scss index 019941534cadd..ed2657ed3f6ee 100644 --- a/src/legacy/server/sass/__fixtures__/index.scss +++ b/src/legacy/server/sass/__fixtures__/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - foo { bar { display: flex; diff --git a/src/legacy/server/sass/build.js b/src/legacy/server/sass/build.js index 2c0a2d84be2c0..536a6dc581db6 100644 --- a/src/legacy/server/sass/build.js +++ b/src/legacy/server/sass/build.js @@ -29,19 +29,15 @@ import isPathInside from 'is-path-inside'; import { PUBLIC_PATH_PLACEHOLDER } from '../../../optimize/public_path_placeholder'; const renderSass = promisify(sass.render); +const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); const access = promisify(fs.access); const copyFile = promisify(fs.copyFile); const mkdirAsync = promisify(fs.mkdir); const UI_ASSETS_DIR = resolve(__dirname, '../../../core/server/core_app/assets'); -const DARK_THEME_IMPORTER = (url) => { - if (url.includes('eui_colors_light')) { - return { file: url.replace('eui_colors_light', 'eui_colors_dark') }; - } - - return { file: url }; -}; +const LIGHT_GLOBALS_PATH = resolve(__dirname, '../../../legacy/ui/public/styles/_globals_v7light'); +const DARK_GLOBALS_PATH = resolve(__dirname, '../../../legacy/ui/public/styles/_globals_v7dark'); const makeAsset = (request, { path, root, boundry, copyRoot, urlRoot }) => { const relativePath = relative(root, path); @@ -84,10 +80,16 @@ export class Build { */ async build() { + const scss = await readFile(this.sourcePath); + const relativeGlobalsPath = + this.theme === 'dark' + ? relative(this.sourceDir, DARK_GLOBALS_PATH) + : relative(this.sourceDir, LIGHT_GLOBALS_PATH); + const rendered = await renderSass({ file: this.sourcePath, + data: `@import '${relativeGlobalsPath}';\n${scss}`, outFile: this.targetPath, - importer: this.theme === 'dark' ? DARK_THEME_IMPORTER : undefined, sourceMap: true, outputStyle: 'nested', sourceMapEmbed: true, diff --git a/src/legacy/server/status/server_status.js b/src/legacy/server/status/server_status.js index 3ee4d37d0b823..81d07de55faaf 100644 --- a/src/legacy/server/status/server_status.js +++ b/src/legacy/server/status/server_status.js @@ -81,7 +81,7 @@ export default class ServerStatus { // reduce to the state with the highest severity, defaulting to green .reduce((a, b) => (a.severity > b.severity ? a : b), states.get('green')); - const statuses = _.where(this._created, { state: state.id }); + const statuses = _.filter(this._created, { state: state.id }); const since = _.get(_.sortBy(statuses, 'since'), [0, 'since']); return { diff --git a/src/legacy/server/status/states.js b/src/legacy/server/status/states.js index bf05f45ff856c..4a34684571c3c 100644 --- a/src/legacy/server/status/states.js +++ b/src/legacy/server/status/states.js @@ -73,7 +73,7 @@ export const getAll = () => [ }, ]; -export const getAllById = () => _.indexBy(exports.getAll(), 'id'); +export const getAllById = () => _.keyBy(exports.getAll(), 'id'); export const defaults = { icon: 'question', diff --git a/src/legacy/ui/public/events.js b/src/legacy/ui/public/events.js index 1dc8a71afb193..464c03d98b83f 100644 --- a/src/legacy/ui/public/events.js +++ b/src/legacy/ui/public/events.js @@ -107,7 +107,7 @@ export function EventsProvider(Promise) { */ Events.prototype.emit = function (name) { const self = this; - const args = _.rest(arguments); + const args = _.tail(arguments); if (!self._listeners[name]) { return self._emitChain; @@ -131,7 +131,7 @@ export function EventsProvider(Promise) { * @return {array[function]} */ Events.prototype.listeners = function (name) { - return _.pluck(this._listeners[name], 'handler'); + return _.map(this._listeners[name], 'handler'); }; return Events; diff --git a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js b/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js index a8abbba9df433..df96a58a6e99f 100644 --- a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js +++ b/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js @@ -30,8 +30,8 @@ const users = [ ]; // this is how we used to accomplish this, before IndexedArray -users.byName = _.indexBy(users, 'name'); -users.byUsername = _.indexBy(users, 'username'); +users.byName = _.keyBy(users, 'name'); +users.byUsername = _.keyBy(users, 'username'); users.byGroup = _.groupBy(users, 'group'); users.inIdOrder = _.sortBy(users, 'id'); @@ -54,7 +54,7 @@ describe('IndexedArray', function () { }); it('clones to an object', function () { - expect(_.isPlainObject(_.clone(reg))).to.be(true); + expect(_.isObject(_.clone(reg))).to.be(true); expect(Array.isArray(_.clone(reg))).to.be(false); }); }); @@ -140,7 +140,7 @@ describe('IndexedArray', function () { reg.remove({ name: 'John' }); - expect(_.eq(reg.raw, reg.slice(0))).to.be(true); + expect(_.isEqual(reg.raw, reg.slice(0))).to.be(true); expect(reg.length).to.be(3); expect(reg[0].name).to.be('Anon'); }); diff --git a/src/legacy/ui/public/indexed_array/indexed_array.js b/src/legacy/ui/public/indexed_array/indexed_array.js index 79ef5e8c183da..b9a427b8da7ad 100644 --- a/src/legacy/ui/public/indexed_array/indexed_array.js +++ b/src/legacy/ui/public/indexed_array/indexed_array.js @@ -52,7 +52,7 @@ export class IndexedArray { this._indexNames = _.union( this._setupIndex(config.group, inflectIndex, organizeByIndexedArray(config)), - this._setupIndex(config.index, inflectIndex, _.indexBy), + this._setupIndex(config.index, inflectIndex, _.keyBy), this._setupIndex(config.order, inflectOrder, (raw, pluckValue) => { return [...raw].sort((itemA, itemB) => { const a = pluckValue(itemA); diff --git a/src/legacy/ui/public/routes/__tests__/_route_manager.js b/src/legacy/ui/public/routes/__tests__/_route_manager.js index 51bde8b8605ac..eb47a3e9ace70 100644 --- a/src/legacy/ui/public/routes/__tests__/_route_manager.js +++ b/src/legacy/ui/public/routes/__tests__/_route_manager.js @@ -46,7 +46,7 @@ describe('routes/route_manager', function () { }) ); - it('should have chainable methods: ' + _.pluck(chainableMethods, 'name').join(', '), function () { + it('should have chainable methods: ' + _.map(chainableMethods, 'name').join(', '), function () { chainableMethods.forEach(function (meth) { expect(routes[meth.name].apply(routes, _.clone(meth.args))).to.be(routes); }); diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index 93428e9f8fa4e..d91834adb4a79 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -341,7 +341,7 @@ export function StateProvider( * @return {object} */ State.prototype.toObject = function () { - return _.omit(this, (value, key) => { + return _.omitBy(this, (value, key) => { return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); }); }; diff --git a/src/legacy/ui/public/styles/_globals_v7dark.scss b/src/legacy/ui/public/styles/_globals_v7dark.scss new file mode 100644 index 0000000000000..d5a8535f32718 --- /dev/null +++ b/src/legacy/ui/public/styles/_globals_v7dark.scss @@ -0,0 +1,12 @@ +// v7dark global scope +// +// prepended to all .scss imports (from JS, when v7dark theme selected) and +// legacy uiExports.styleSheetPaths when any dark theme is selected + +@import '@elastic/eui/src/themes/eui/eui_colors_dark'; + +@import '@elastic/eui/src/global_styling/functions/index'; +@import '@elastic/eui/src/global_styling/variables/index'; +@import '@elastic/eui/src/global_styling/mixins/index'; + +@import './mixins'; diff --git a/src/legacy/ui/public/styles/_styling_constants.scss b/src/legacy/ui/public/styles/_globals_v7light.scss similarity index 59% rename from src/legacy/ui/public/styles/_styling_constants.scss rename to src/legacy/ui/public/styles/_globals_v7light.scss index 74fc54b410285..522b346b64900 100644 --- a/src/legacy/ui/public/styles/_styling_constants.scss +++ b/src/legacy/ui/public/styles/_globals_v7light.scss @@ -1,9 +1,10 @@ -// EUI global scope +// v7light global scope +// +// prepended to all .scss imports (from JS, when v7light theme selected) and +// legacy uiExports.styleSheetPaths when any dark theme is selected @import '@elastic/eui/src/themes/eui/eui_colors_light'; -// Note that fonts are loaded directly by src/legacy/ui/ui_render/views/chrome.pug - @import '@elastic/eui/src/global_styling/functions/index'; @import '@elastic/eui/src/global_styling/variables/index'; @import '@elastic/eui/src/global_styling/mixins/index'; diff --git a/src/legacy/ui/public/styles/_globals_v8dark.scss b/src/legacy/ui/public/styles/_globals_v8dark.scss new file mode 100644 index 0000000000000..972365e9e9d0e --- /dev/null +++ b/src/legacy/ui/public/styles/_globals_v8dark.scss @@ -0,0 +1,16 @@ +// v8dark global scope +// +// prepended to all .scss imports (from JS, when v8dark theme selected) + +@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_dark'; + +@import '@elastic/eui/src/global_styling/functions/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index'; + +@import '@elastic/eui/src/global_styling/variables/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index'; + +@import '@elastic/eui/src/global_styling/mixins/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index'; + +@import './mixins'; diff --git a/src/legacy/ui/public/styles/_globals_v8light.scss b/src/legacy/ui/public/styles/_globals_v8light.scss new file mode 100644 index 0000000000000..dc99f4d45082e --- /dev/null +++ b/src/legacy/ui/public/styles/_globals_v8light.scss @@ -0,0 +1,16 @@ +// v8light global scope +// +// prepended to all .scss imports (from JS, when v8light theme selected) + +@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_light'; + +@import '@elastic/eui/src/global_styling/functions/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index'; + +@import '@elastic/eui/src/global_styling/variables/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index'; + +@import '@elastic/eui/src/global_styling/mixins/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index'; + +@import './mixins'; diff --git a/src/legacy/ui/public/utils/collection.ts b/src/legacy/ui/public/utils/collection.ts index 45e5a0704c37b..b882a2bbe6e5b 100644 --- a/src/legacy/ui/public/utils/collection.ts +++ b/src/legacy/ui/public/utils/collection.ts @@ -50,7 +50,7 @@ export function move( } below = !!below; - qualifier = qualifier && _.callback(qualifier); + qualifier = qualifier && _.iteratee(qualifier); const above = !below; const finder = below ? _.findIndex : _.findLastIndex; diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index ca2e944489a73..bbca051ce31a1 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,7 +1,6 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; -window.__kbnDarkMode__ = {{darkMode}}; -window.__kbnThemeVersion__ = "{{themeVersion}}"; +window.__kbnThemeTag__ = "{{themeTag}}"; window.__kbnPublicPath__ = {{publicPathMap}}; window.__kbnBundles__ = {{kbnBundlesLoaderSource}} diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 0cfcb91aa94ef..b4b18e086e809 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -89,6 +89,7 @@ export function uiRenderMixin(kbnServer, server, config) { const isCore = !app; const uiSettings = request.getUiSettingsService(); + const darkMode = !authEnabled || request.auth.isAuthenticated ? await uiSettings.get('theme:darkMode') @@ -99,6 +100,8 @@ export function uiRenderMixin(kbnServer, server, config) { ? await uiSettings.get('theme:version') : 'v7'; + const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`; + const buildHash = server.newPlatform.env.packageInfo.buildNum; const basePath = config.get('server.basePath'); @@ -178,8 +181,7 @@ export function uiRenderMixin(kbnServer, server, config) { const bootstrap = new AppBootstrap({ templateData: { - darkMode, - themeVersion, + themeTag, jsDependencyPaths, styleSheetPaths, publicPathMap, diff --git a/src/legacy/utils/deep_clone_with_buffers.ts b/src/legacy/utils/deep_clone_with_buffers.ts index 2e9120eb32b7c..2c58d85187985 100644 --- a/src/legacy/utils/deep_clone_with_buffers.ts +++ b/src/legacy/utils/deep_clone_with_buffers.ts @@ -17,7 +17,7 @@ * under the License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeepWith } from 'lodash'; // We should add `any` return type to overcome bug in lodash types, customizer // in lodash 3.* can return `undefined` if cloning is handled by the lodash, but @@ -29,5 +29,5 @@ function cloneBuffersCustomizer(val: unknown): any { } export function deepCloneWithBuffers(val: T): T { - return cloneDeep(val, cloneBuffersCustomizer); + return cloneDeepWith(val, cloneBuffersCustomizer); } diff --git a/src/legacy/utils/unset.js b/src/legacy/utils/unset.js index 8b4cc0a7be1cd..db6f0e5ea9ef1 100644 --- a/src/legacy/utils/unset.js +++ b/src/legacy/utils/unset.js @@ -18,11 +18,10 @@ */ import _ from 'lodash'; -import toPath from 'lodash/internal/toPath'; export function unset(object, rawPath) { if (!object) return; - const path = toPath(rawPath); + const path = _.toPath(rawPath); switch (path.length) { case 0: diff --git a/src/plugins/charts/public/services/colors/mapped_colors.test.ts b/src/plugins/charts/public/services/colors/mapped_colors.test.ts index 2c9f37afc14c5..e97ca8ac257b4 100644 --- a/src/plugins/charts/public/services/colors/mapped_colors.test.ts +++ b/src/plugins/charts/public/services/colors/mapped_colors.test.ts @@ -61,7 +61,7 @@ describe('Mapped Colors', () => { mappedColors.mapKeys(arr); const colorValues = _(mappedColors.mapping).values(); - expect(colorValues.contains(seedColors[0])).toBe(false); + expect(colorValues.includes(seedColors[0])).toBe(false); expect(colorValues.uniq().size()).toBe(arr.length); }); diff --git a/src/plugins/charts/public/services/colors/mapped_colors.ts b/src/plugins/charts/public/services/colors/mapped_colors.ts index fe0deac734e6b..3b9e1501d638d 100644 --- a/src/plugins/charts/public/services/colors/mapped_colors.ts +++ b/src/plugins/charts/public/services/colors/mapped_colors.ts @@ -54,7 +54,7 @@ export class MappedColors { } get(key: string | number) { - return this.getConfigColorMapping()[key] || this._mapping[key]; + return this.getConfigColorMapping()[key as any] || this._mapping[key]; } flush() { @@ -75,10 +75,10 @@ export class MappedColors { const keysToMap: Array = []; _.each(keys, (key) => { // If this key is mapped in the config, it's unnecessary to have it mapped here - if (configMapping[key]) delete this._mapping[key]; + if (configMapping[key as any]) delete this._mapping[key]; // If this key is mapped to a color used by the config color mapping, we need to remap it - if (_.contains(configColors, this._mapping[key])) keysToMap.push(key); + if (_.includes(configColors, this._mapping[key])) keysToMap.push(key); // if key exist in oldMap, move it to mapping if (this._oldMap[key]) this._mapping[key] = this._oldMap[key]; @@ -93,7 +93,7 @@ export class MappedColors { let newColors = _.difference(colorPalette, allColors); while (keysToMap.length > newColors.length) { - newColors = newColors.concat(_.sample(allColors, keysToMap.length - newColors.length)); + newColors = newColors.concat(_.sampleSize(allColors, keysToMap.length - newColors.length)); } _.merge(this._mapping, _.zipObject(keysToMap, newColors)); diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index 377e739a0c59a..ebcc2a35b6111 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -17,6 +17,7 @@ * under the License. */ +import _ from 'lodash'; import React, { Fragment, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index b7cc8f2f4b72f..06823a981af46 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -117,7 +117,7 @@ describe('Integration', () => { return t; }); if (terms.length !== expectedTerms.length) { - expect(_.pluck(terms, 'name')).toEqual(_.pluck(expectedTerms, 'name')); + expect(_.map(terms, 'name')).toEqual(_.map(expectedTerms, 'name')); } else { const filteredActualTerms = _.map(terms, function (actualTerm, i) { const expectedTerm = expectedTerms[i]; diff --git a/src/plugins/console/public/lib/autocomplete/body_completer.js b/src/plugins/console/public/lib/autocomplete/body_completer.js index f37b3ac0cca9c..d31507626146e 100644 --- a/src/plugins/console/public/lib/autocomplete/body_completer.js +++ b/src/plugins/console/public/lib/autocomplete/body_completer.js @@ -51,7 +51,7 @@ function resolvePathToComponents(tokenPath, context, editor, components) { context, editor ); - const result = [].concat.apply([], _.pluck(walkStates, 'components')); + const result = [].concat.apply([], _.map(walkStates, 'components')); return result; } diff --git a/src/plugins/console/public/lib/autocomplete/components/list_component.js b/src/plugins/console/public/lib/autocomplete/components/list_component.js index b770638a61ff7..b26a223433333 100644 --- a/src/plugins/console/public/lib/autocomplete/components/list_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/list_component.js @@ -62,7 +62,7 @@ export class ListComponent extends SharedComponent { // verify we have all tokens const list = this.listGenerator(); - const notFound = _.any(tokens, function (token) { + const notFound = _.some(tokens, function (token) { return list.indexOf(token) === -1; }); diff --git a/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js b/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js index 79a332624e5e1..412fda16d45b6 100644 --- a/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js +++ b/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js @@ -61,73 +61,64 @@ export class UrlPatternMatcher { } const endpointComponents = endpoint.url_components || {}; const partList = pattern.split('/'); - _.each( - partList, - function (part, partIndex) { - if (part.search(/^{.+}$/) >= 0) { - part = part.substr(1, part.length - 2); - if (activeComponent.getComponent(part)) { - // we already have something for this, reuse - activeComponent = activeComponent.getComponent(part); - return; - } - // a new path, resolve. + _.each(partList, (part, partIndex) => { + if (part.search(/^{.+}$/) >= 0) { + part = part.substr(1, part.length - 2); + if (activeComponent.getComponent(part)) { + // we already have something for this, reuse + activeComponent = activeComponent.getComponent(part); + return; + } + // a new path, resolve. - if ((c = endpointComponents[part])) { - // endpoint specific. Support list - if (Array.isArray(c)) { - c = new ListComponent(part, c, activeComponent); - } else if (_.isObject(c) && c.type === 'list') { - c = new ListComponent( - part, - c.list, - activeComponent, - c.multiValued, - c.allow_non_valid - ); - } else { - console.warn( - 'incorrectly configured url component ', - part, - ' in endpoint', - endpoint - ); - c = new SharedComponent(part); - } - } else if ((c = this[method].parametrizedComponentFactories.getComponent(part))) { - // c is a f - c = c(part, activeComponent); + if ((c = endpointComponents[part])) { + // endpoint specific. Support list + if (Array.isArray(c)) { + c = new ListComponent(part, c, activeComponent); + } else if (_.isObject(c) && c.type === 'list') { + c = new ListComponent( + part, + c.list, + activeComponent, + c.multiValued, + c.allow_non_valid + ); } else { - // just accept whatever with not suggestions - c = new SimpleParamComponent(part, activeComponent); + console.warn('incorrectly configured url component ', part, ' in endpoint', endpoint); + c = new SharedComponent(part); } - - activeComponent = c; + } else if ((c = this[method].parametrizedComponentFactories.getComponent(part))) { + // c is a f + c = c(part, activeComponent); } else { - // not pattern - let lookAhead = part; - let s; + // just accept whatever with not suggestions + c = new SimpleParamComponent(part, activeComponent); + } - for (partIndex++; partIndex < partList.length; partIndex++) { - s = partList[partIndex]; - if (s.indexOf('{') >= 0) { - break; - } - lookAhead += '/' + s; - } + activeComponent = c; + } else { + // not pattern + let lookAhead = part; + let s; - if (activeComponent.getComponent(part)) { - // we already have something for this, reuse - activeComponent = activeComponent.getComponent(part); - activeComponent.addOption(lookAhead); - } else { - c = new ConstantComponent(part, activeComponent, lookAhead); - activeComponent = c; + for (partIndex++; partIndex < partList.length; partIndex++) { + s = partList[partIndex]; + if (s.indexOf('{') >= 0) { + break; } + lookAhead += '/' + s; } - }, - this - ); + + if (activeComponent.getComponent(part)) { + // we already have something for this, reuse + activeComponent = activeComponent.getComponent(part); + activeComponent.addOption(lookAhead); + } else { + c = new ConstantComponent(part, activeComponent, lookAhead); + activeComponent = c; + } + } + }); // mark end of endpoint path new AcceptEndpointComponent(endpoint, activeComponent); }); diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 38be0d8a7e4c9..b893218f4967c 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -26,16 +26,12 @@ export function wrapComponentWithDefaults(component, defaults) { if (!result) { return result; } - result = _.map( - result, - function (term) { - if (!_.isObject(term)) { - term = { name: term }; - } - return _.defaults(term, defaults); - }, - this - ); + result = _.map(result, (term) => { + if (!_.isObject(term)) { + term = { name: term }; + } + return _.defaults(term, defaults); + }); return result; }; return component; @@ -145,7 +141,7 @@ export function populateContext(tokenPath, context, editor, includeAutoComplete, }); }); }); - autoCompleteSet = _.uniq(autoCompleteSet, false); + autoCompleteSet = _.uniq(autoCompleteSet); context.autoCompleteSet = autoCompleteSet; } diff --git a/src/plugins/console/public/lib/autocomplete/url_params.js b/src/plugins/console/public/lib/autocomplete/url_params.js index a237fe5dd59d6..037f4b1b27c50 100644 --- a/src/plugins/console/public/lib/autocomplete/url_params.js +++ b/src/plugins/console/public/lib/autocomplete/url_params.js @@ -50,18 +50,14 @@ export class UrlParams { } description = _.clone(description || {}); _.defaults(description, defaults); - _.each( - description, - function (pDescription, param) { - const component = new ParamComponent(param, this.rootComponent, pDescription); - if (Array.isArray(pDescription)) { - new ListComponent(param, pDescription, component); - } else if (pDescription === '__flag__') { - new ListComponent(param, ['true', 'false'], component); - } - }, - this - ); + _.each(description, (pDescription, param) => { + const component = new ParamComponent(param, this.rootComponent, pDescription); + if (Array.isArray(pDescription)) { + new ListComponent(param, pDescription, component); + } else if (pDescription === '__flag__') { + new ListComponent(param, ['true', 'false'], component); + } + }); } getTopLevelComponents() { return this.rootComponent.next; diff --git a/src/plugins/console/public/lib/kb/api.js b/src/plugins/console/public/lib/kb/api.js index aafb234b0f446..0e3b6a345836b 100644 --- a/src/plugins/console/public/lib/kb/api.js +++ b/src/plugins/console/public/lib/kb/api.js @@ -60,19 +60,15 @@ function Api(urlParametrizedComponentFactories, bodyParametrizedComponentFactori cls.addEndpointDescription = function (endpoint, description) { const copiedDescription = {}; - _.extend(copiedDescription, description || {}); + _.assign(copiedDescription, description || {}); _.defaults(copiedDescription, { id: endpoint, patterns: [endpoint], methods: ['GET'], }); - _.each( - copiedDescription.patterns, - function (p) { - this.urlPatternMatcher.addEndpoint(p, copiedDescription); - }, - this - ); + _.each(copiedDescription.patterns, (p) => { + this.urlPatternMatcher.addEndpoint(p, copiedDescription); + }); copiedDescription.paramsAutocomplete = new UrlParams(copiedDescription.url_params); copiedDescription.bodyAutocompleteRootComponents = compileBodyDescription( diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index 22aae8da030d4..88fe195bcbf2b 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -98,7 +98,7 @@ export function getFields(indices, types) { ret = [].concat.apply([], ret); } - return _.uniq(ret, function (f) { + return _.uniqBy(ret, function (f) { return f.name + ':' + f.type; }); } @@ -191,7 +191,7 @@ function getFieldNamesFromProperties(properties = {}) { }); // deduping - return _.uniq(fieldList, function (f) { + return _.uniqBy(fieldList, function (f) { return f.name + ':' + f.type; }); } diff --git a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts index 28a971794d403..38592e66bd8be 100644 --- a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts +++ b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts @@ -25,7 +25,7 @@ import url from 'url'; import { ESConfigForProxy } from '../types'; const createAgent = (legacyConfig: ESConfigForProxy) => { - const target = url.parse(_.head(legacyConfig.hosts)); + const target = url.parse(_.head(legacyConfig.hosts) as any); if (!/^https/.test(target.protocol || '')) return new http.Agent(); const agentOptions: https.AgentOptions = {}; diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index 272f63322ffaa..a16fb1dadfbcf 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -19,7 +19,7 @@ import { Agent, IncomingMessage } from 'http'; import * as url from 'url'; -import { pick, trimLeft, trimRight } from 'lodash'; +import { pick, trimStart, trimEnd } from 'lodash'; import { KibanaRequest, Logger, RequestHandler } from 'kibana/server'; @@ -46,7 +46,7 @@ export interface CreateHandlerDependencies { } function toURL(base: string, path: string) { - const urlResult = new url.URL(`${trimRight(base, '/')}/${trimLeft(path, '/')}`); + const urlResult = new url.URL(`${trimEnd(base, '/')}/${trimStart(path, '/')}`); // Appending pretty here to have Elasticsearch do the JSON formatting, as doing // in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of // measurement precision) diff --git a/src/plugins/console/server/services/spec_definitions_service.ts b/src/plugins/console/server/services/spec_definitions_service.ts index ccd3b6b1c0a82..ce990e62a2284 100644 --- a/src/plugins/console/server/services/spec_definitions_service.ts +++ b/src/plugins/console/server/services/spec_definitions_service.ts @@ -55,11 +55,11 @@ export class SpecDefinitionsService { }); if (urlParamsDef) { - description.url_params = _.extend(description.url_params || {}, copiedDescription.url_params); + description.url_params = _.assign(description.url_params || {}, copiedDescription.url_params); _.defaults(description.url_params, urlParamsDef); } - _.extend(copiedDescription, description); + _.assign(copiedDescription, description); _.defaults(copiedDescription, { id: endpoint, patterns: [endpoint], diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index 96210358c05e4..26af13b4410fe 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from 'src/core/public'; import uuid from 'uuid'; +import _ from 'lodash'; import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { ViewMode, PanelState, IEmbeddable } from '../../embeddable_plugin'; import { SavedObject } from '../../../../saved_objects/public'; diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx index 57fe4acf08145..e4a98ffac7a55 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; +import _ from 'lodash'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import { NotificationsStart, Toast } from 'src/core/public'; import { DashboardPanelState } from '../embeddable'; diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 58477d28f9081..a321bc7959c5c 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -17,7 +17,7 @@ * under the License. */ -import _, { uniq } from 'lodash'; +import _, { uniqBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CANCEL_BUTTON, EuiCheckboxGroup } from '@elastic/eui'; import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/form/checkbox/checkbox_group'; @@ -265,7 +265,7 @@ export class DashboardAppController { if (!embeddableIndexPatterns) return; panelIndexPatterns.push(...embeddableIndexPatterns); }); - panelIndexPatterns = uniq(panelIndexPatterns, 'id'); + panelIndexPatterns = uniqBy(panelIndexPatterns, 'id'); if (panelIndexPatterns && panelIndexPatterns.length > 0) { $scope.$evalAsync(() => { @@ -520,7 +520,7 @@ export class DashboardAppController { differences.filters = appStateDashboardInput.filters; } - Object.keys(_.omit(containerInput, 'filters')).forEach((key) => { + Object.keys(_.omit(containerInput, ['filters'])).forEach((key) => { const containerValue = (containerInput as { [key: string]: unknown })[key]; const appStateValue = ((appStateDashboardInput as unknown) as { [key: string]: unknown })[ key diff --git a/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts b/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts index 79116a57869d3..a6928c0608bd2 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts @@ -17,7 +17,6 @@ * under the License. */ -import _ from 'lodash'; import { PanelState, EmbeddableInput } from '../../../embeddable_plugin'; import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants'; import { DashboardPanelState } from '../types'; diff --git a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts index 1b060c186db97..5ecd57d670ae8 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts @@ -17,6 +17,7 @@ * under the License. */ +import _ from 'lodash'; import { PanelNotFoundError } from '../../../embeddable_plugin'; import { GridData } from '../../../../common'; import { DashboardPanelState, DASHBOARD_GRID_COLUMN_COUNT } from '..'; diff --git a/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts b/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts index e3b6725ce7449..72d3ffe6b2322 100644 --- a/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts @@ -47,7 +47,7 @@ export function updateSavedDashboard( 'pause', 'section', 'value', - ]); + ]) as RefreshInterval; savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; // save only unpinned filters diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index 4f7945d6dd601..1e8356a1ef100 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -111,7 +111,7 @@ export const dashboardSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow>(migrateMatchAllQuery), - '7.0.0': flow>(migrations700), - '7.3.0': flow>(migrations730), + '6.7.2': flow(migrateMatchAllQuery), + '7.0.0': flow(migrations700), + '7.3.0': flow(migrations730), }; diff --git a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts index 75e169b79f320..452d68aa92394 100644 --- a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts @@ -22,7 +22,7 @@ import { get } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; export const migrateMatchAllQuery: SavedObjectMigrationFn = (doc) => { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { let searchSource: any; diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts index ae9d1c7921955..261977b859659 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isEqual, clone } from 'lodash'; +import { isEqual, cloneDeep } from 'lodash'; import { migrateFilter, DeprecatedMatchPhraseFilter } from './migrate_filter'; import { PhraseFilter, MatchAllFilter } from '../filters'; @@ -52,7 +52,7 @@ describe('migrateFilter', function () { }); it('should not modify the original filter', function () { - const oldMatchPhraseFilterCopy = clone(oldMatchPhraseFilter, true); + const oldMatchPhraseFilterCopy = cloneDeep(oldMatchPhraseFilter); migrateFilter(oldMatchPhraseFilter, undefined); diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts index 990d588359442..4441155ad9218 100644 --- a/src/plugins/data/common/es_query/filters/index.ts +++ b/src/plugins/data/common/es_query/filters/index.ts @@ -44,6 +44,6 @@ export * from './types'; * @param {object} filter The filter to clean * @returns {object} */ -export const cleanFilter = (filter: Filter): Filter => omit(filter, ['meta', '$state']); +export const cleanFilter = (filter: Filter): Filter => omit(filter, ['meta', '$state']) as Filter; export const isFilterDisabled = (filter: Filter): boolean => get(filter, 'meta.disabled', false); diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index c318a0f0c2c3d..c355a7397797c 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { map, reduce, mapValues, get, keys, pick } from 'lodash'; +import { map, reduce, mapValues, get, keys, pickBy } from 'lodash'; import { Filter, FilterMeta } from './meta_filter'; import { IIndexPattern, IFieldType } from '../../index_patterns'; @@ -112,7 +112,7 @@ export const buildRangeFilter = ( filter.meta.formattedValue = formattedValue; } - params = mapValues(params, (value) => (field.type === 'number' ? parseFloat(value) : value)); + params = mapValues(params, (value: any) => (field.type === 'number' ? parseFloat(value) : value)); if ('gte' in params && 'gt' in params) throw new Error('gte and gt are mutually exclusive'); if ('lte' in params && 'lt' in params) throw new Error('lte and lt are mutually exclusive'); @@ -148,7 +148,7 @@ export const buildRangeFilter = ( }; export const getRangeScript = (field: IFieldType, params: RangeFilterParams) => { - const knownParams = pick(params, (val, key: any) => key in operators); + const knownParams = pickBy(params, (val, key: any) => key in operators); let script = map( knownParams, (val: any, key: string) => '(' + field.script + ')' + get(operators, key) + key diff --git a/src/plugins/data/common/es_query/kuery/functions/is.ts b/src/plugins/data/common/es_query/kuery/functions/is.ts index 89aec6e55e81b..404f27b38992c 100644 --- a/src/plugins/data/common/es_query/kuery/functions/is.ts +++ b/src/plugins/data/common/es_query/kuery/functions/is.ts @@ -97,7 +97,7 @@ export function toElasticsearchQuery( }); } - const isExistsQuery = valueArg.type === 'wildcard' && value === '*'; + const isExistsQuery = valueArg.type === 'wildcard' && (value as any) === '*'; const isAllFieldsQuery = (fullFieldNameArg.type === 'wildcard' && ((fieldName as unknown) as string) === '*') || (fields && indexPattern && fields.length === indexPattern.fields.length); @@ -135,7 +135,7 @@ export function toElasticsearchQuery( ...accumulator, { script: { - ...getPhraseScript(field, value), + ...getPhraseScript(field, value as any), }, }, ]; diff --git a/src/plugins/data/common/field_formats/converters/truncate.ts b/src/plugins/data/common/field_formats/converters/truncate.ts index a6c4a1133a2ed..c9ab9df920e16 100644 --- a/src/plugins/data/common/field_formats/converters/truncate.ts +++ b/src/plugins/data/common/field_formats/converters/truncate.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { trunc } from 'lodash'; +import { truncate } from 'lodash'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; @@ -35,7 +35,7 @@ export class TruncateFormat extends FieldFormat { textConvert: TextContextTypeConvert = (val) => { const length = this.param('fieldLength'); if (length > 0) { - return trunc(val, { + return truncate(val, { length: length + omission.length, omission, }); diff --git a/src/plugins/data/common/field_formats/field_format.test.ts b/src/plugins/data/common/field_formats/field_format.test.ts index 2229601994496..2b8f9ad48a34b 100644 --- a/src/plugins/data/common/field_formats/field_format.test.ts +++ b/src/plugins/data/common/field_formats/field_format.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { constant, trimRight, trimLeft, get } from 'lodash'; +import { constant, trimEnd, trimStart, get } from 'lodash'; import { FieldFormat } from './field_format'; import { asPrettyString } from './utils'; @@ -120,8 +120,8 @@ describe('FieldFormat class', () => { test('does escape the output of the text converter if used in an html context', () => { const f = getTestFormat(undefined, constant('')); - const expected = trimRight( - trimLeft(f.convert('', 'html'), ''), + const expected = trimEnd( + trimStart(f.convert('', 'html'), ''), '' ); diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 26f07a12067ce..9e4308d6fd559 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -185,7 +185,7 @@ export abstract class FieldFormat { const params = transform( this._params, - (uniqParams, val, param) => { + (uniqParams: any, val, param) => { if (param && val !== get(defaultsParams, param)) { uniqParams[param] = val; } diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 9325485bce75d..74a942b51583d 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -233,7 +233,7 @@ export class FieldFormatsRegistry { parseDefaultTypeMap(value: any) { this.defaultMap = value; forOwn(this, (fn) => { - if (isFunction(fn) && fn.cache) { + if (isFunction(fn) && (fn as any).cache) { // clear all memoize caches // @ts-ignore fn.cache = new memoize.Cache(); diff --git a/src/plugins/data/common/field_mapping/mapping_setup.ts b/src/plugins/data/common/field_mapping/mapping_setup.ts index 99b49b401a8b8..0bad47d9889f0 100644 --- a/src/plugins/data/common/field_mapping/mapping_setup.ts +++ b/src/plugins/data/common/field_mapping/mapping_setup.ts @@ -28,7 +28,7 @@ type ShorthandFieldMapObject = FieldMappingSpec | ES_FIELD_TYPES | 'json'; /** @public */ export const expandShorthand = (sh: Record): MappingObject => { - return mapValues>(sh, (val: ShorthandFieldMapObject) => { + return mapValues(sh, (val: ShorthandFieldMapObject) => { const fieldMap = isString(val) ? { type: val } : val; const json: FieldMappingSpec = { type: ES_FIELD_TYPES.TEXT, diff --git a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts index 26f1a185ada3a..1702441aa4ca1 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts @@ -17,7 +17,7 @@ * under the License. */ -import { contains } from 'lodash'; +import { includes } from 'lodash'; import { IndexPatternsContract } from './index_patterns'; import { UiSettingsCommon } from '../types'; @@ -35,7 +35,7 @@ export const createEnsureDefaultIndexPattern = ( const patterns = await this.getIds(); let defaultId = await uiSettings.get('defaultIndex'); let defined = !!defaultId; - const exists = contains(patterns, defaultId); + const exists = includes(patterns, defaultId); if (defined && !exists) { await uiSettings.remove('defaultIndex'); diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts index c194687b7c3bf..c1aa2efe46998 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts @@ -77,7 +77,7 @@ function decorateFlattenedWrapper(hit: Record, metaFields: Record { const scriptedNames = mockLogStashFields() .filter((item: Field) => item.scripted === true) .map((item: Field) => item.name); - const respNames = pluck(indexPattern.getScriptedFields(), 'name'); + const respNames = map(indexPattern.getScriptedFields(), 'name'); expect(respNames).toEqual(scriptedNames); }); @@ -216,7 +216,7 @@ describe('IndexPattern', () => { const notScriptedNames = mockLogStashFields() .filter((item: Field) => item.scripted === false) .map((item: Field) => item.name); - const respNames = pluck(indexPattern.getNonScriptedFields(), 'name'); + const respNames = map(indexPattern.getNonScriptedFields(), 'name'); expect(respNames).toEqual(notScriptedNames); }); @@ -287,7 +287,7 @@ describe('IndexPattern', () => { // const saveSpy = sinon.spy(indexPattern, 'save'); const scriptedFields = indexPattern.getScriptedFields(); const oldCount = scriptedFields.length; - const scriptedField = last(scriptedFields); + const scriptedField = last(scriptedFields) as any; await indexPattern.removeScriptedField(scriptedField); @@ -298,7 +298,7 @@ describe('IndexPattern', () => { test('should not allow duplicate names', async () => { const scriptedFields = indexPattern.getScriptedFields(); - const scriptedField = last(scriptedFields); + const scriptedField = last(scriptedFields) as any; expect.assertions(1); try { await indexPattern.addScriptedField(scriptedField.name, "'new script'", 'string', 'lang'); 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 bde550c660a32..dab11ad0ce29a 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 @@ -353,9 +353,9 @@ export class IndexPattern implements IIndexPattern { async addScriptedField(name: string, script: string, fieldType: string = 'string', lang: string) { const scriptedFields = this.getScriptedFields(); - const names = _.pluck(scriptedFields, 'name'); + const names = _.map(scriptedFields, 'name'); - if (_.contains(names, name)) { + if (_.includes(names, name)) { throw new DuplicateField(name); } @@ -417,11 +417,11 @@ export class IndexPattern implements IIndexPattern { } getNonScriptedFields() { - return _.where(this.fields, { scripted: false }); + return _.filter(this.fields, { scripted: false }); } getScriptedFields() { - return _.where(this.fields, { scripted: true }); + return _.filter(this.fields, { scripted: true }); } isTimeBased(): boolean { diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts index 65df6e26a25b3..be8e9b13e7cff 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -44,7 +44,7 @@ const mapFilter = ( comparators: FilterCompareOptions, excludedAttributes: string[] ) => { - const cleaned: FilterMeta = omit(filter, excludedAttributes); + const cleaned: FilterMeta = omit(filter, excludedAttributes) as FilterMeta; if (comparators.index) cleaned.index = filter.meta?.index; if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); diff --git a/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts index 8f1d02c5ffd54..345dd3b32691e 100644 --- a/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts +++ b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts @@ -27,12 +27,11 @@ import { type SOClient = Pick; -const simpleSavedObjectToSavedObject = ( - simpleSavedObject: SimpleSavedObject -): SavedObject => ({ - version: simpleSavedObject._version, - ...omit(simpleSavedObject, '_version'), -}); +const simpleSavedObjectToSavedObject = (simpleSavedObject: SimpleSavedObject): SavedObject => + ({ + version: simpleSavedObject._version, + ...omit(simpleSavedObject, '_version'), + } as any); export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommon { private savedObjectClient: SOClient; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index f19611bc1d526..670b40e7d9472 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -584,8 +584,8 @@ export abstract class FieldFormat { textConvert: TextContextTypeConvert | undefined; static title: string; toJSON(): { - id: unknown; - params: _.Dictionary | undefined; + id: any; + params: any; }; type: any; } diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index 60a49a4bd50f4..eaf6ddc9afc35 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -65,7 +65,7 @@ export class FilterManager { } // matching filter in globalState, update global and don't add from appState - _.assign(match.meta, filter.meta); + _.assignIn(match.meta, filter.meta); }); return FilterManager.mergeFilters(cleanedAppFilters, globalFilters); diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts index 432a763bfd48c..723001297e8f2 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -53,7 +53,7 @@ function getExistingFilter( if (isScriptedPhraseFilter(filter)) { return filter.meta.field === fieldName && filter.script!.script.params.value === value; } - }); + }) as any; } function updateExistingFilter(existingFilter: Filter, negate: boolean) { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts index d2d5a4b069218..41457a01e0c97 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_range.ts @@ -17,7 +17,7 @@ * under the License. */ -import { get, has } from 'lodash'; +import { get, hasIn } from 'lodash'; import { FilterValueFormatter, RangeFilter, @@ -48,10 +48,10 @@ function getParams(filter: RangeFilter) { ? get(filter, 'script.script.params') : getRangeByKey(filter, key); - let left = has(params, 'gte') ? params.gte : params.gt; + let left = hasIn(params, 'gte') ? params.gte : params.gt; if (left == null) left = -Infinity; - let right = has(params, 'lte') ? params.lte : params.lt; + let right = hasIn(params, 'lte') ? params.lte : params.lt; if (right == null) right = Infinity; const value = getFormattedValueFn(left, right); diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index 8650f5920e520..de49e9ab6f66e 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -271,7 +271,7 @@ export class AggConfig { const outParams = _.transform( this.getAggParams(), - (out, aggParam) => { + (out: any, aggParam) => { let val = params[aggParam.name]; // don't serialize undefined/null values @@ -365,7 +365,7 @@ export class AggConfig { } getAggParams() { - return [...(_.has(this, 'type.params') ? this.type.params : [])]; + return [...(_.hasIn(this, 'type.params') ? this.type.params : [])]; } getRequestAggs() { @@ -438,14 +438,10 @@ export class AggConfig { public set type(type) { if (this.__typeDecorations) { - _.forOwn( - this.__typeDecorations, - function (prop, name: string | undefined) { - // @ts-ignore - delete this[name]; - }, - this - ); + _.forOwn(this.__typeDecorations, (prop, name: string | undefined) => { + // @ts-ignore + delete this[name]; + }); } if (type && _.isFunction(type.decorateAggConfig)) { diff --git a/src/plugins/data/public/search/aggs/agg_configs.test.ts b/src/plugins/data/public/search/aggs/agg_configs.test.ts index 121bb29f6f8ed..f3efeb028665b 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; import { AggConfig } from './agg_config'; import { AggConfigs } from './agg_configs'; import { AggTypesRegistryStart } from './agg_types_registry'; @@ -166,7 +166,7 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); const sorted = ac.getRequestAggs(); - const aggs = indexBy(ac.aggs, (agg) => agg.type.name); + const aggs = keyBy(ac.aggs, (agg) => agg.type.name); expect(sorted.shift()).toBe(aggs.terms); expect(sorted.shift()).toBe(aggs.histogram); @@ -189,7 +189,7 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); const sorted = ac.getResponseAggs(); - const aggs = indexBy(ac.aggs, (agg) => agg.type.name); + const aggs = keyBy(ac.aggs, (agg) => agg.type.name); expect(sorted.shift()).toBe(aggs.terms); expect(sorted.shift()).toBe(aggs.date_histogram); @@ -206,7 +206,7 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); const sorted = ac.getResponseAggs(); - const aggs = indexBy(ac.aggs, (agg) => agg.type.name); + const aggs = keyBy(ac.aggs, (agg) => agg.type.name); expect(sorted.shift()).toBe(aggs.terms); expect(sorted.shift()).toBe(aggs.date_histogram); diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index 4052c0b390155..cb17ef07a930b 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -90,7 +90,7 @@ export const getFiltersBucketAgg = ({ const outFilters = transform( inFilters, - function (filters, filter) { + function (filters: any, filter) { const input = cloneDeep(filter.input); if (!input) { diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range.ts b/src/plugins/data/public/search/aggs/buckets/ip_range.ts index 018fcb365b585..ed9bc5e0462f1 100644 --- a/src/plugins/data/public/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/ip_range.ts @@ -17,7 +17,7 @@ * under the License. */ -import { noop, map, omit, isNull } from 'lodash'; +import { noop, map, omitBy, isNull } from 'lodash'; import { i18n } from '@kbn/i18n'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -101,7 +101,7 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA let ranges = aggConfig.params.ranges[ipRangeType]; if (ipRangeType === IP_RANGE_TYPES.FROM_TO) { - ranges = map(ranges, (range: any) => omit(range, isNull)); + ranges = map(ranges, (range: any) => omitBy(range, isNull)); } output.params.ranges = ranges; diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts index 12197c85f4a96..017f646258c01 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -113,7 +113,7 @@ export class TimeBuckets { bounds = Array.isArray(input) ? input : []; } - const moments: Moment[] = sortBy(bounds, Number); + const moments: Moment[] = sortBy(bounds, Number) as Moment[]; const valid = moments.length === 2 && moments.every(isValidMoment); if (!valid) { diff --git a/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts b/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts index 47da7e59af5e0..8dc8b786fcfcd 100644 --- a/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts +++ b/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts @@ -52,7 +52,7 @@ export const migrateIncludeExcludeFormat = { output.params[this.name] = parsedValue; } } else if (isObject(value)) { - output.params[this.name] = value.pattern; + output.params[this.name] = (value as any).pattern; } else if (value && isStringType(aggConfig)) { output.params[this.name] = value; } diff --git a/src/plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts b/src/plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts index 00d866e6f2b3e..25d3a3ea90a4a 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts @@ -17,7 +17,7 @@ * under the License. */ -import { assign } from 'lodash'; +import { assignIn } from 'lodash'; import { IMetricAggConfig } from '../metric_agg_type'; /** @@ -69,7 +69,7 @@ export const create = (parentAgg: IMetricAggConfig, props: Partial + isObject(nsValue) ? {} : nsValue + ); } const esQueryConfigs = getEsQueryConfig(uiSettings); @@ -460,7 +473,7 @@ export class SearchSource { ]); let serializedSearchSourceFields: SearchSourceFields = { ...searchSourceFields, - index: searchSourceFields.index ? searchSourceFields.index.id : undefined, + index: (searchSourceFields.index ? searchSourceFields.index.id : undefined) as any, }; if (originalFilters) { const filters = this.getFilters(originalFilters); diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 87f53e1c72079..8582f4a12fa38 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -33,7 +33,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useState, Fragment, useRef } from 'react'; +import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react'; import { sortBy } from 'lodash'; import { SavedQuery, SavedQueryService } from '../..'; import { SavedQueryListItem } from './saved_query_list_item'; @@ -88,9 +88,51 @@ export function SavedQueryManagementComponent({ } }, [isOpen, activePage, savedQueryService]); - const goToPage = (pageNumber: number) => { - setActivePage(pageNumber); - }; + const handleTogglePopover = useCallback(() => setIsOpen((currentState) => !currentState), [ + setIsOpen, + ]); + + const handleClosePopover = useCallback(() => setIsOpen(false), []); + + const handleSave = useCallback(() => { + handleClosePopover(); + onSave(); + }, [handleClosePopover, onSave]); + + const handleSaveAsNew = useCallback(() => { + handleClosePopover(); + onSaveAsNew(); + }, [handleClosePopover, onSaveAsNew]); + + const handleSelect = useCallback( + (savedQueryToSelect) => { + handleClosePopover(); + onLoad(savedQueryToSelect); + }, + [handleClosePopover, onLoad] + ); + + const handleDelete = useCallback( + (savedQueryToDelete: SavedQuery) => { + const onDeleteSavedQuery = async (savedQuery: SavedQuery) => { + cancelPendingListingRequest.current(); + setSavedQueries( + savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQuery.id) + ); + + if (loadedSavedQuery && loadedSavedQuery.id === savedQuery.id) { + onClearSavedQuery(); + } + + await savedQueryService.deleteSavedQuery(savedQuery.id); + setActivePage(0); + }; + + onDeleteSavedQuery(savedQueryToDelete); + handleClosePopover(); + }, + [handleClosePopover, loadedSavedQuery, onClearSavedQuery, savedQueries, savedQueryService] + ); const savedQueryDescriptionText = i18n.translate( 'data.search.searchBar.savedQueryDescriptionText', @@ -113,25 +155,13 @@ export function SavedQueryManagementComponent({ } ); - const onDeleteSavedQuery = async (savedQuery: SavedQuery) => { - cancelPendingListingRequest.current(); - setSavedQueries( - savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQuery.id) - ); - - if (loadedSavedQuery && loadedSavedQuery.id === savedQuery.id) { - onClearSavedQuery(); - } - - await savedQueryService.deleteSavedQuery(savedQuery.id); - setActivePage(0); + const goToPage = (pageNumber: number) => { + setActivePage(pageNumber); }; const savedQueryPopoverButton = ( { - setIsOpen(!isOpen); - }} + onClick={handleTogglePopover} aria-label={i18n.translate('data.search.searchBar.savedQueryPopoverButtonText', { defaultMessage: 'See saved queries', })} @@ -159,11 +189,8 @@ export function SavedQueryManagementComponent({ key={savedQuery.id} savedQuery={savedQuery} isSelected={!!loadedSavedQuery && loadedSavedQuery.id === savedQuery.id} - onSelect={(savedQueryToSelect) => { - onLoad(savedQueryToSelect); - setIsOpen(false); - }} - onDelete={(savedQueryToDelete) => onDeleteSavedQuery(savedQueryToDelete)} + onSelect={handleSelect} + onDelete={handleDelete} showWriteOperations={!!showSaveQuery} /> )); @@ -175,9 +202,7 @@ export function SavedQueryManagementComponent({ id="savedQueryPopover" button={savedQueryPopoverButton} isOpen={isOpen} - closePopover={() => { - setIsOpen(false); - }} + closePopover={handleClosePopover} anchorPosition="downLeft" panelPaddingSize="none" buffer={-8} @@ -235,7 +260,7 @@ export function SavedQueryManagementComponent({ onSave()} + onClick={handleSave} aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel', { @@ -256,7 +281,7 @@ export function SavedQueryManagementComponent({ onSaveAsNew()} + onClick={handleSaveAsNew} aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel', { @@ -280,7 +305,7 @@ export function SavedQueryManagementComponent({ onSave()} + onClick={handleSave} aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel', { defaultMessage: 'Save a new saved query' } @@ -299,7 +324,7 @@ export function SavedQueryManagementComponent({ onClearSavedQuery()} + onClick={onClearSavedQuery} aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverClearButtonAriaLabel', { defaultMessage: 'Clear current saved query' } diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index a0df7604f23aa..f8b7e4f480911 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -17,6 +17,7 @@ * under the License. */ +import _ from 'lodash'; import React, { useState, useEffect, useRef } from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index 502364cdcba32..b4b86b73a5f4a 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -17,7 +17,7 @@ * under the License. */ -import { defaults, indexBy, sortBy } from 'lodash'; +import { defaults, keyBy, sortBy } from 'lodash'; import { LegacyAPICaller } from 'kibana/server'; import { callFieldCapsApi } from '../es_api'; @@ -44,7 +44,7 @@ export async function getFieldCapabilities( metaFields: string[] = [] ) { const esFieldCaps: FieldCapsResponse = await callFieldCapsApi(callCluster, indices); - const fieldsFromFieldCapsByName = indexBy(readFieldCapsResponse(esFieldCaps), 'name'); + const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) .filter((name) => !name.startsWith('_')) diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts index a01d34dbe9df6..2e408d7569be5 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts @@ -46,7 +46,7 @@ export async function resolveTimePattern(callCluster: LegacyAPICaller, timePatte [] ) .sortBy((indexName: string) => indexName) - .uniq(true) + .sortedUniq() .map((indexName) => { const parsed = moment(indexName, timePattern, true); if (!parsed.isValid()) { @@ -65,7 +65,7 @@ export async function resolveTimePattern(callCluster: LegacyAPICaller, timePatte isMatch: indexName === parsed.format(timePattern), }; }) - .sortByOrder(['valid', 'order'], ['desc', 'desc']) + .orderBy(['valid', 'order'], ['desc', 'desc']) .value(); return { diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts index 37819a13b6518..768041a376ad1 100644 --- a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts @@ -55,6 +55,6 @@ const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = }; export const indexPatternSavedObjectTypeMigrations = { - '6.5.0': flow(migrateAttributeTypeAndAttributeTypeMeta), - '7.6.0': flow(migrateSubTypeAndParentFieldProperties), + '6.5.0': flow(migrateAttributeTypeAndAttributeTypeMeta), + '7.6.0': flow(migrateSubTypeAndParentFieldProperties), }; diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index 902cf2988f429..44d2813f6e3e8 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -54,5 +54,5 @@ export const indexPatternSavedObjectType: SavedObjectsType = { typeMeta: { type: 'keyword' }, }, }, - migrations: indexPatternSavedObjectTypeMigrations, + migrations: indexPatternSavedObjectTypeMigrations as any, }; diff --git a/src/plugins/data/server/saved_objects/search.ts b/src/plugins/data/server/saved_objects/search.ts index 437c83f67bf5d..16caaf05a0fc6 100644 --- a/src/plugins/data/server/saved_objects/search.ts +++ b/src/plugins/data/server/saved_objects/search.ts @@ -56,5 +56,5 @@ export const searchSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: searchSavedObjectTypeMigrations, + migrations: searchSavedObjectTypeMigrations as any, }; diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts index 2e37cd1255cee..9bba429f8d71b 100644 --- a/src/plugins/data/server/saved_objects/search_migrations.ts +++ b/src/plugins/data/server/saved_objects/search_migrations.ts @@ -22,7 +22,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../common'; const migrateMatchAllQuery: SavedObjectMigrationFn = (doc) => { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { let searchSource: any; @@ -122,7 +122,7 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; export const searchSavedObjectTypeMigrations = { - '6.7.2': flow>(migrateMatchAllQuery), - '7.0.0': flow>(setNewReferences), - '7.4.0': flow>(migrateSearchSortToNestedArray), + '6.7.2': flow(migrateMatchAllQuery), + '7.0.0': flow(setNewReferences), + '7.4.0': flow(migrateSearchSortToNestedArray), }; diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover/public/application/angular/context/query/actions.js index 0e057e0a715c4..32fc2873d7f2a 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.js +++ b/src/plugins/discover/public/application/angular/context/query/actions.js @@ -70,7 +70,7 @@ export function QueryActionsProvider(Promise) { setLoadingStatus(state)('anchor'); return Promise.try(() => - fetchAnchor(indexPatternId, anchorId, [_.zipObject([sort]), { [tieBreakerField]: sort[1] }]) + fetchAnchor(indexPatternId, anchorId, [_.fromPairs([sort]), { [tieBreakerField]: sort[1] }]) ).then( (anchorDocument) => { setLoadedStatus(state)('anchor'); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts index 875cbf4075aa2..87401818c4907 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts @@ -186,7 +186,7 @@ describe('fieldCalculator', function () { expect(extensions).toBeInstanceOf(Object); expect(extensions.buckets).toBeInstanceOf(Array); expect(extensions.buckets.length).toBe(3); - expect(_.pluck(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']); + expect(_.map(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']); expect(extensions.error).toBe(undefined); }); diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index 2c6bbcc3ecce1..ecb5d7fd90283 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ + +import _ from 'lodash'; import { createHashHistory } from 'history'; import { ScopedHistory } from 'kibana/public'; import { DiscoverServices } from './build_services'; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js index 18e9ffcb27c56..cde2a253d7630 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js @@ -19,7 +19,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; -import { padLeft } from 'lodash'; +import { padStart } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -55,12 +55,12 @@ function makeSequence(min, max) { const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({ value: value.toString(), - text: padLeft(value, 2, '0'), + text: padStart(value, 2, '0'), })); const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({ value: value.toString(), - text: padLeft(value, 2, '0'), + text: padStart(value, 2, '0'), })); const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({ diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx index 5667220881df2..39b91a2e20b53 100644 --- a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx +++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx @@ -23,7 +23,7 @@ import { WithMultiContent, useMultiContentContext, HookProps } from '../multi_co export interface Props { onSave: (data: T) => void | Promise; - children: JSX.Element | JSX.Element[]; + children: JSX.Element | Array; isEditing?: boolean; defaultActiveStep?: number; defaultValue?: HookProps['defaultValue']; diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx b/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx index 5fbe3d2bbbdd4..210b0cedccd06 100644 --- a/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx +++ b/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx @@ -54,7 +54,7 @@ export function useMultiContentContext(contentId: keyof T) { +export function useContent(contentId: K) { const { updateContentAt, saveSnapshotAndRemoveContent, getData } = useMultiContentContext(); const updateContent = useCallback( @@ -71,8 +71,11 @@ export function useContent(contentId: }; }, [contentId, saveSnapshotAndRemoveContent]); + const data = getData(); + const defaultValue = data[contentId]; + return { - defaultValue: getData()[contentId]!, + defaultValue, updateContent, getData, }; diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts index 0a2c7bb651959..adc68a39a4a5b 100644 --- a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts +++ b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts @@ -150,6 +150,10 @@ export function useMultiContent({ * Validate the multi-content active content(s) in the DOM */ const validate = useCallback(async () => { + if (Object.keys(contents.current).length === 0) { + return Boolean(validation.isValid); + } + const updatedValidation = {} as { [key in keyof T]?: boolean | undefined }; for (const [id, _content] of Object.entries(contents.current)) { diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 28baa3d8372f0..67c1ee3c7d677 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -22,6 +22,7 @@ * In the future, each top level folder should be exported like that to avoid naming collision */ import * as Forms from './forms'; +import * as Monaco from './monaco'; export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor'; @@ -53,10 +54,6 @@ export { expandLiteralStrings, } from './console_lang'; -import * as Monaco from './monaco'; - -export { Monaco }; - export { AuthorizationContext, AuthorizationProvider, @@ -69,7 +66,7 @@ export { useAuthorizationContext, } from './authorization'; -export { Forms }; +export { Monaco, Forms }; /** dummy plugin, we just want esUiShared to have its own bundle */ export function plugin() { diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts index dc8321aa07004..019a0e8053d0d 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts @@ -21,12 +21,13 @@ import { ValidationFunc } from '../../hook_form_lib'; import { isJSON } from '../../../validators/string'; import { ERROR_CODE } from './types'; -export const isJsonField = (message: string) => ( - ...args: Parameters -): ReturnType> => { +export const isJsonField = ( + message: string, + { allowEmptyString = false }: { allowEmptyString?: boolean } = {} +) => (...args: Parameters): ReturnType> => { const [{ value }] = args; - if (typeof value !== 'string') { + if (typeof value !== 'string' || (allowEmptyString && value.trim() === '')) { return; } diff --git a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts b/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts index b8be273d7bbd3..2b7d1b8ed9d76 100644 --- a/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts +++ b/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { uniq } from 'lodash'; +import { uniqBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../../expression_functions'; import { KibanaContext } from '../../expression_types'; @@ -40,7 +40,7 @@ const getParsedValue = (data: any, defaultValue: any) => typeof data === 'string' && data.length ? JSON.parse(data) || defaultValue : defaultValue; const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) => - uniq( + uniqBy( [...(Array.isArray(first) ? first : [first]), ...(Array.isArray(second) ? second : [second])], (n: any) => JSON.stringify(n.query) ); diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index c113765f8e7e7..5cd53df663e1d 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -20,7 +20,7 @@ import { map, pick, zipObject } from 'lodash'; import { ExpressionTypeDefinition } from '../types'; -import { PointSeries } from './pointseries'; +import { PointSeries, PointSeriesColumn } from './pointseries'; import { ExpressionValueRender } from './render'; const name = 'datatable'; @@ -109,8 +109,8 @@ export const datatable: ExpressionTypeDefinition ({ type: name, rows: value.rows, - columns: map(value.columns, (val, colName) => { - return { name: colName!, type: val.type }; + columns: map(value.columns, (val: PointSeriesColumn, colName) => { + return { name: colName, type: val.type }; }), }), }, diff --git a/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts index 7f2f3c37c587c..e226f3b124eed 100644 --- a/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts @@ -19,7 +19,7 @@ import { map } from 'lodash'; import { SerializedFieldFormat } from '../../types/common'; -import { Datatable, PointSeries } from '.'; +import { Datatable, PointSeries, PointSeriesColumn } from '.'; const name = 'kibana_datatable'; @@ -62,7 +62,7 @@ export const kibanaDatatable = { }; }, pointseries: (context: PointSeries) => { - const columns = map(context.columns, (column, n) => { + const columns = map(context.columns, (column: PointSeriesColumn, n) => { return { id: n, name: n, ...column }; }); return { diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 9428d7db1d9d0..f957f10a9aeba 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -19,6 +19,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; +import { defaults } from 'lodash'; import { Adapters } from '../../inspector/public'; import { IExpressionLoaderParams } from './types'; import { ExpressionAstExpression } from '../common'; @@ -168,7 +169,7 @@ export class ExpressionLoader { } if (params.searchContext) { - this.params.searchContext = _.defaults( + this.params.searchContext = defaults( {}, params.searchContext, this.params.searchContext || {} diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index 52cd5b0c3f5bd..5ab9c695caaa0 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Dictionary, countBy, defaults, unique } from 'lodash'; +import { Dictionary, countBy, defaults, uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public'; import { IndexPatternManagementStart } from '../../../../../../plugins/index_pattern_management/public'; @@ -145,7 +145,7 @@ export function convertToEuiSelectOption(options: string[], type: string) { ] : []; return euiOptions.concat( - unique(options).map((option) => { + uniq(options).map((option) => { return { value: option, text: option, diff --git a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 4eff5112c0c07..03ed6c5520dec 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -86,11 +86,11 @@ export class PhraseFilterManager extends FilterManager { private getValueFromFilter(kbnFilter: PhraseFilter): any { // bool filter - multiple phrase filters if (_.has(kbnFilter, 'query.bool.should')) { - return _.get(kbnFilter, 'query.bool.should') - .map((kbnQueryFilter) => { + return _.get(kbnFilter, 'query.bool.should') + .map((kbnQueryFilter: PhraseFilter) => { return this.getValueFromFilter(kbnQueryFilter); }) - .filter((value) => { + .filter((value: any) => { if (value) { return true; } diff --git a/src/plugins/inspector/common/adapters/request/request_adapter.ts b/src/plugins/inspector/common/adapters/request/request_adapter.ts index 70af6b5b51d18..af10d1b77b16d 100644 --- a/src/plugins/inspector/common/adapters/request/request_adapter.ts +++ b/src/plugins/inspector/common/adapters/request/request_adapter.ts @@ -18,7 +18,6 @@ */ import { EventEmitter } from 'events'; -import _ from 'lodash'; import uuid from 'uuid/v4'; import { RequestResponder } from './request_responder'; import { Request, RequestParams, RequestStatus } from './types'; diff --git a/src/plugins/inspector/public/views/data/lib/export_csv.ts b/src/plugins/inspector/public/views/data/lib/export_csv.ts index c0e0153c6053e..5a970cc6cff38 100644 --- a/src/plugins/inspector/public/views/data/lib/export_csv.ts +++ b/src/plugins/inspector/public/views/data/lib/export_csv.ts @@ -29,7 +29,7 @@ const allDoubleQuoteRE = /"/g; function escape(val: string, quoteValues: boolean) { if (isObject(val)) { - val = val.valueOf(); + val = (val as any).valueOf(); } val = String(val); diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 87fdf0730c880..2fa1debf51b5c 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { debounce, indexBy, sortBy, uniq } from 'lodash'; +import { debounce, keyBy, sortBy, uniq } from 'lodash'; import { EuiTitle, EuiInMemoryTable, @@ -178,7 +178,7 @@ class TableListView extends React.Component itemsById[id])); } catch (error) { this.props.toastNotifications.addDanger({ diff --git a/src/plugins/kibana_utils/common/url/encode_uri_query.ts b/src/plugins/kibana_utils/common/url/encode_uri_query.ts index fb60f0ceff10f..fe8cf12d0d6f2 100644 --- a/src/plugins/kibana_utils/common/url/encode_uri_query.ts +++ b/src/plugins/kibana_utils/common/url/encode_uri_query.ts @@ -45,7 +45,7 @@ export const encodeQuery = ( query: ParsedQuery, encodeFunction: (val: string, pctEncodeSpaces?: boolean) => string = encodeUriQuery ) => - transform(query, (result, value, key) => { + transform(query, (result: any, value, key) => { if (key) { const singleValue = Array.isArray(value) ? value.join(',') : value; diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 3a05ce59f5d13..1c5642f9b75b7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { capitalize, isFunction } from 'lodash'; +import { upperFirst, isFunction } from 'lodash'; import React, { MouseEvent } from 'react'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; @@ -50,11 +50,11 @@ export function TopNavMenuItem(props: TopNavMenuData) { const btn = props.emphasize ? ( - {capitalize(props.label || props.id!)} + {upperFirst(props.label || props.id!)} ) : ( - {capitalize(props.label || props.id!)} + {upperFirst(props.label || props.id!)} ); diff --git a/src/plugins/saved_objects_management/public/lib/create_field_list.ts b/src/plugins/saved_objects_management/public/lib/create_field_list.ts index 5f424751dd58e..dcfb44d8a5224 100644 --- a/src/plugins/saved_objects_management/public/lib/create_field_list.ts +++ b/src/plugins/saved_objects_management/public/lib/create_field_list.ts @@ -17,7 +17,7 @@ * under the License. */ -import { forOwn, indexBy, isNumber, isBoolean, isPlainObject, isString } from 'lodash'; +import { forOwn, keyBy, isNumber, isBoolean, isPlainObject, isString } from 'lodash'; import { SimpleSavedObject } from '../../../../core/public'; import { castEsToKbnFieldTypeName } from '../../../data/public'; import { ObjectField } from '../management_section/types'; @@ -93,9 +93,9 @@ const addFieldsFromClass = function ( Class: { mapping: Record; searchSource: any }, fields: ObjectField[] ) { - const fieldMap = indexBy(fields, 'name'); + const fieldMap = keyBy(fields, 'name'); - _.forOwn(Class.mapping, (esType, name) => { + forOwn(Class.mapping, (esType, name) => { if (!name || fieldMap[name]) { return; } diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index 75692777f08bb..dbbea4012aba9 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -78,7 +78,7 @@ const SavedObjectsTablePage = ({ }} canGoInApp={(savedObject) => { const { inAppUrl } = savedObject.meta; - return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false; + return inAppUrl ? Boolean(get(capabilities, inAppUrl.uiCapabilitiesPath)) : false; }} /> ); diff --git a/src/plugins/tile_map/public/index.scss b/src/plugins/tile_map/public/index.scss index 4ce500b2da4d2..f4b86b0c31190 100644 --- a/src/plugins/tile_map/public/index.scss +++ b/src/plugins/tile_map/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "tlm" to avoid conflicts. // Examples // tlmChart diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 39abddb3de853..ef2f937c8547c 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -87,7 +87,7 @@ function getAggParamsToRender({ // should be refactored in the future to provide a more general way // for visualization to override some agg config settings if (agg.type.name === 'top_hits' && param.name === 'field') { - const allowStrings = _.get(schema, `aggSettings[${agg.type.name}].allowStrings`, false); + const allowStrings = get(schema, `aggSettings[${agg.type.name}].allowStrings`, false); if (!allowStrings) { availableFields = availableFields.filter((field) => field.type === 'number'); } diff --git a/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx index a0bc0d78a2889..37e95f2419b45 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/input_list.tsx @@ -18,7 +18,7 @@ */ import React, { useState, useEffect, Fragment, useCallback } from 'react'; -import { isEmpty, isEqual, mapValues, omit, pick } from 'lodash'; +import { isEmpty, isEqual, mapValues, omitBy, pick } from 'lodash'; import { EuiButtonIcon, EuiFlexGroup, @@ -173,7 +173,7 @@ function InputList({ config, list, onChange, setValidity }: InputListProps) { const model: InputObject = mapValues(pick(models[index], modelNames), 'model'); // we need to skip empty values since they are not stored in saved object - return !isEqual(item, omit(model, isEmpty)); + return !isEqual(item, omitBy(model, isEmpty)); }) ) { setModels( diff --git a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts index 6eaef3050029a..a3998cbd5954b 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts @@ -105,7 +105,7 @@ function validateValueUnique( } function getNextModel(list: NumberRowModel[], range: NumberListRange): NumberRowModel { - const lastValue = last(list).value; + const lastValue = (last(list) as NumberRowModel).value; let next = Number(lastValue) ? Number(lastValue) + 1 : 1; if (next >= range.max) { diff --git a/src/plugins/vis_default_editor/public/components/controls/filters.tsx b/src/plugins/vis_default_editor/public/components/controls/filters.tsx index 9a9933b5e1e83..04d0df27927fa 100644 --- a/src/plugins/vis_default_editor/public/components/controls/filters.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filters.tsx @@ -43,7 +43,9 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps { // set parsed values into model after initialization - setValue(filters.map((filter) => omit({ ...filter, input: filter.input }, 'id'))); + setValue( + filters.map((filter) => omit({ ...filter, input: filter.input }, 'id') as FilterValue) + ); }); useEffect(() => { @@ -58,7 +60,7 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps { // do not set internal id parameter into saved object - setValue(updatedFilters.map((filter) => omit(filter, 'id'))); + setValue(updatedFilters.map((filter) => omit(filter, 'id') as FilterValue)); setFilters(updatedFilters); }; diff --git a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx index 0d21eb04c12b2..f6354027ab01b 100644 --- a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx @@ -56,7 +56,7 @@ function NumberIntervalParamEditor({ setValidity, setValue, }: AggParamEditorProps) { - const base: number = get(editorConfig, 'interval.base'); + const base: number = get(editorConfig, 'interval.base') as number; const min = base || 0; const isValid = value !== undefined && value >= min; diff --git a/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx index 4af41f67bc524..dd9e432fa512e 100644 --- a/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx @@ -107,7 +107,7 @@ function TimeIntervalParamEditor({ setTouched, setValidity, }: AggParamEditorProps) { - const timeBase: string = get(editorConfig, 'interval.timeBase'); + const timeBase: string = get(editorConfig, 'interval.timeBase') as string; const options = timeBase ? [] : ((aggParam as any).options || []).reduce( diff --git a/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx index 26567d05e0427..b2c7bcafa15a3 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/data_tab.tsx @@ -74,7 +74,8 @@ function DefaultEditorDataTab({ ), [metricAggs] ); - const lastParentPipelineAggTitle = lastParentPipelineAgg && lastParentPipelineAgg.type.title; + const lastParentPipelineAggTitle = + lastParentPipelineAgg && (lastParentPipelineAgg as IAggConfig).type.title; const addSchema: AddSchema = useCallback((schema) => dispatch(addNewAgg(schema)), [dispatch]); @@ -116,7 +117,7 @@ function DefaultEditorDataTab({ setValidity, setTouched, removeAgg: onAggRemove, - }; + } as any; return ( <> diff --git a/src/plugins/vis_default_editor/public/schemas.ts b/src/plugins/vis_default_editor/public/schemas.ts index 54520b85cb5ec..d95a6252331bf 100644 --- a/src/plugins/vis_default_editor/public/schemas.ts +++ b/src/plugins/vis_default_editor/public/schemas.ts @@ -58,6 +58,7 @@ export class Schemas implements ISchemas { > ) { _(schemas || []) + .chain() .map((schema) => { if (!schema.name) throw new Error('all schema must have a unique name'); diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx index 5e8a463748188..4385826762612 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx @@ -28,6 +28,7 @@ import { getHeatmapColors } from '../../../charts/public'; import { VisParams, MetricVisMetric } from '../types'; import { getFormatService } from '../services'; import { SchemaConfig, ExprVis } from '../../../visualizations/public'; +import { Range } from '../../../expressions/public'; export interface MetricVisComponentProps { visParams: VisParams; @@ -41,7 +42,7 @@ export class MetricVisComponent extends Component { const config = this.props.visParams.metric; const isPercentageMode = config.percentageMode; const colorsRange = config.colorsRange; - const max = last(colorsRange).to; + const max = (last(colorsRange) as Range).to; const labels: string[] = []; colorsRange.forEach((range: any) => { @@ -111,7 +112,7 @@ export class MetricVisComponent extends Component { const dimensions = this.props.visParams.dimensions; const isPercentageMode = config.percentageMode; const min = config.colorsRange[0].from; - const max = last(config.colorsRange).to; + const max = (last(config.colorsRange) as Range).to; const colors = this.getColors(); const labels = this.getLabels(); const metrics: MetricVisMetric[] = []; diff --git a/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts index db29d9112be8e..860b4e9f2dbde 100644 --- a/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts +++ b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { cloneDeep, defaults, merge, compact } from 'lodash'; +import { cloneDeep, defaults, mergeWith, compact } from 'lodash'; import moment, { Moment } from 'moment-timezone'; import { TimefilterContract } from 'src/plugins/data/public'; @@ -91,7 +91,7 @@ function buildSeriesData(chart: Series[], options: jquery.flot.plotOptions) { } if (series._global) { - merge(options, series._global, (objVal, srcVal) => { + mergeWith(options, series._global, (objVal, srcVal) => { // This is kind of gross, it means that you can't replace a global value with a null // best you can do is an empty string. Deal with it. if (objVal == null) { diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts index c02f43818af9c..7be18a4774d94 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -27,6 +27,7 @@ import { import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; import { TIMELION_VIS_NAME } from './timelion_vis_type'; import { TimelionVisDependencies } from './plugin'; +import { Filter, Query, TimeRange } from '../../data/common'; type Input = KibanaContext | null; type Output = Promise>; @@ -71,9 +72,9 @@ export const getTimelionVisualizationConfig = ( const visParams = { expression: args.expression, interval: args.interval }; const response = await timelionRequestHandler({ - timeRange: get(input, 'timeRange'), - query: get(input, 'query'), - filters: get(input, 'filters'), + timeRange: get(input, 'timeRange') as TimeRange, + query: get(input, 'query') as Query, + filters: get(input, 'filters') as Filter[], visParams, forceFetch: true, }); diff --git a/src/plugins/vis_type_timelion/server/fit_functions/average.js b/src/plugins/vis_type_timelion/server/fit_functions/average.js index 06db7bd0e9324..09518a3286487 100644 --- a/src/plugins/vis_type_timelion/server/fit_functions/average.js +++ b/src/plugins/vis_type_timelion/server/fit_functions/average.js @@ -27,7 +27,7 @@ export default function average(dataTuples, targetTuples) { // Phase 1: Downsample // We necessarily won't well match the dataSource here as we don't know how much data // they had when creating their own average - const resultTimes = _.pluck(targetTuples, 0); + const resultTimes = _.map(targetTuples, 0); const dataTuplesQueue = _.clone(dataTuples); const resultValues = _.map(targetTuples, function (bucket) { const time = bucket[0]; diff --git a/src/plugins/vis_type_timelion/server/handlers/chain_runner.js b/src/plugins/vis_type_timelion/server/handlers/chain_runner.js index 59adea30730c7..2ee8deb4dd049 100644 --- a/src/plugins/vis_type_timelion/server/handlers/chain_runner.js +++ b/src/plugins/vis_type_timelion/server/handlers/chain_runner.js @@ -132,7 +132,7 @@ export default function chainRunner(tlConfig) { }); }); return Bluebird.all(seriesList).then(function (args) { - const list = _.chain(args).pluck('list').flatten().value(); + const list = _.chain(args).map('list').flatten().value(); const seriesList = _.merge.apply(this, _.flatten([{}, args])); seriesList.list = list; return seriesList; diff --git a/src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js b/src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js index 9b4fdddc2186e..11004d2784d3c 100644 --- a/src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js +++ b/src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js @@ -28,7 +28,7 @@ export default function validateArgFn(functionDef) { const multi = argDef.multi; const isCorrectType = (function () { // If argument is not allow to be specified multiple times, we're dealing with a plain value for type - if (!multi) return _.contains(required, type); + if (!multi) return _.includes(required, type); // If it is, we'll get an array for type return _.difference(type, required).length === 0; })(); diff --git a/src/plugins/vis_type_timelion/server/lib/as_sorted.js b/src/plugins/vis_type_timelion/server/lib/as_sorted.js index 536145a6b8dcd..6a2b7c0f5a9f5 100644 --- a/src/plugins/vis_type_timelion/server/lib/as_sorted.js +++ b/src/plugins/vis_type_timelion/server/lib/as_sorted.js @@ -22,5 +22,5 @@ import unzipPairs from './unzip_pairs.js'; export default function asSorted(timeValObject, fn) { const data = unzipPairs(timeValObject); - return _.zipObject(fn(data)); + return _.fromPairs(fn(data)); } diff --git a/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js b/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js index 83466e263cf2f..3d53fc8d5bd09 100644 --- a/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js +++ b/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js @@ -25,7 +25,7 @@ export default class TimelionFunction { constructor(name, config) { this.name = name; this.args = config.args || []; - this.argsByName = _.indexBy(this.args, 'name'); + this.argsByName = _.keyBy(this.args, 'name'); this.help = config.help || ''; this.aliases = config.aliases || []; this.extended = config.extended || false; diff --git a/src/plugins/vis_type_timelion/server/lib/load_functions.js b/src/plugins/vis_type_timelion/server/lib/load_functions.js index d6cb63b7c427b..699342cff6a79 100644 --- a/src/plugins/vis_type_timelion/server/lib/load_functions.js +++ b/src/plugins/vis_type_timelion/server/lib/load_functions.js @@ -47,7 +47,7 @@ export default function (directory) { }) .value(); - const functions = _.zipObject(files.concat(directories)); + const functions = _.fromPairs(files.concat(directories)); _.each(functions, function (func) { _.assign(functions, processFunctionDefinition(func)); diff --git a/src/plugins/vis_type_timelion/server/lib/reduce.js b/src/plugins/vis_type_timelion/server/lib/reduce.js index cc13b75fde12d..1a5d78676fc72 100644 --- a/src/plugins/vis_type_timelion/server/lib/reduce.js +++ b/src/plugins/vis_type_timelion/server/lib/reduce.js @@ -42,7 +42,7 @@ async function pairwiseReduce(left, right, fn) { if (allSeriesContainKey(left, 'split') && allSeriesContainKey(right, 'split')) { pairwiseField = 'split'; } - const indexedList = _.indexBy(right.list, pairwiseField); + const indexedList = _.keyBy(right.list, pairwiseField); // ensure seriesLists contain same pairwise labels left.list.forEach((leftSeries) => { diff --git a/src/plugins/vis_type_timelion/server/lib/unzip_pairs.js b/src/plugins/vis_type_timelion/server/lib/unzip_pairs.js index 7a34b5ec98ff0..412049c89ef2f 100644 --- a/src/plugins/vis_type_timelion/server/lib/unzip_pairs.js +++ b/src/plugins/vis_type_timelion/server/lib/unzip_pairs.js @@ -21,7 +21,7 @@ import _ from 'lodash'; export default function unzipPairs(timeValObject) { const paired = _.chain(timeValObject) - .pairs() + .toPairs() .map(function (point) { return [parseInt(point[0], 10), point[1]]; }) diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js index 409372da24724..fbae9c5afffe8 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js @@ -20,7 +20,7 @@ import _ from 'lodash'; export function timeBucketsToPairs(buckets) { - const timestamps = _.pluck(buckets, 'key'); + const timestamps = _.map(buckets, 'key'); const series = {}; _.each(buckets, function (bucket) { _.forOwn(bucket, function (val, key) { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js index bc0e368fbdab1..e407636c41567 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js @@ -50,7 +50,7 @@ export default function buildRequest(config, tlConfig, scriptedFields, timeout) .map(function (q) { return [q, { query_string: { query: q } }]; }) - .zipObject() + .fromPairs() .value(), }, aggs: {}, diff --git a/src/plugins/vis_type_timelion/server/series_functions/movingaverage.js b/src/plugins/vis_type_timelion/server/series_functions/movingaverage.js index 108eb0c72f19d..fdaa4dcd8c098 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/movingaverage.js +++ b/src/plugins/vis_type_timelion/server/series_functions/movingaverage.js @@ -81,7 +81,7 @@ export default new Chainable('movingaverage', { } _position = _position || defaultPosition; - if (!_.contains(validPositions, _position)) { + if (!_.includes(validPositions, _position)) { throw new Error( i18n.translate( 'timelion.serverSideErrors.movingaverageFunction.notValidPositionErrorMessage', diff --git a/src/plugins/vis_type_timelion/server/series_functions/movingstd.js b/src/plugins/vis_type_timelion/server/series_functions/movingstd.js index a7ecb4d5b4738..2b9ab08f02ede 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/movingstd.js +++ b/src/plugins/vis_type_timelion/server/series_functions/movingstd.js @@ -61,7 +61,7 @@ export default new Chainable('movingstd', { return alter(args, function (eachSeries, _window, _position) { _position = _position || defaultPosition; - if (!_.contains(positions, _position)) { + if (!_.includes(positions, _position)) { throw new Error( i18n.translate( 'timelion.serverSideErrors.movingstdFunction.notValidPositionErrorMessage', diff --git a/src/plugins/vis_type_timelion/server/series_functions/points.js b/src/plugins/vis_type_timelion/server/series_functions/points.js index bf797bb5aa343..74d616cffd52d 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/points.js +++ b/src/plugins/vis_type_timelion/server/series_functions/points.js @@ -105,7 +105,7 @@ export default new Chainable('points', { } symbol = symbol || defaultSymbol; - if (!_.contains(validSymbols, symbol)) { + if (!_.includes(validSymbols, symbol)) { throw new Error( i18n.translate('timelion.serverSideErrors.pointsFunction.notValidSymbolErrorMessage', { defaultMessage: 'Valid symbols are: {validSymbols}', diff --git a/src/plugins/vis_type_timelion/server/series_functions/static.test.js b/src/plugins/vis_type_timelion/server/series_functions/static.test.js index 88ec9fecd904a..36c5dc708f860 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/static.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/static.test.js @@ -26,7 +26,7 @@ import invoke from './helpers/invoke_series_fn.js'; describe('static.js', () => { it('returns a series in which all numbers are the same', () => { return invoke(fn, [5]).then((r) => { - expect(_.unique(_.map(r.output.list[0].data, 1))).to.eql([5]); + expect(_.uniq(_.map(r.output.list[0].data, 1))).to.eql([5]); }); }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx index 0363ba486a775..fcb22a9e79707 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx @@ -39,7 +39,7 @@ interface AggRowProps { export function AggRow(props: AggRowProps) { let iconType = 'eyeClosed'; let iconColor = 'subdued'; - const lastSibling = last(props.siblings); + const lastSibling = last(props.siblings) as MetricsItemsSchema; if (lastSibling.id === props.model.id) { iconType = 'eye'; 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 beb691f4b7117..0638c6e67f5ef 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 @@ -31,7 +31,7 @@ export const seriesChangeHandler = (props, items) => (doc) => { const metric = newMetricAggFn(); metric.type = doc.type; const incompatPipelines = ['calculation', 'series_agg']; - if (!_.contains(incompatPipelines, doc.type)) metric.field = doc.id; + if (!_.includes(incompatPipelines, doc.type)) metric.field = doc.id; return metric; }); } else { diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index fd20ff8b024b3..0f0d99bff6f1c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { uniq } from 'lodash'; +import { uniqBy } from 'lodash'; import { first, map } from 'rxjs/operators'; import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; @@ -87,5 +87,5 @@ export async function getFields( (field) => field.aggregatable && !indexPatterns.isNestedField(field) ); - return uniq(fields, (field) => field.name); + return uniqBy(fields, (field) => field.name); } diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index f6754e5fd9ca4..a9b542af68c9d 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -40,7 +40,7 @@ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { }, }, migrations: { - '7.7.0': flow(resetCount), - '7.8.0': flow(resetCount), + '7.7.0': flow(resetCount), + '7.8.0': flow(resetCount), }, }; diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index a9c915fcfb636..6b1af6044a2c4 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; +import { TimeRange, Query } from '../../data/public'; type Input = KibanaContext | null; type Output = Promise>; @@ -58,9 +59,9 @@ export const createVegaFn = ( const vegaRequestHandler = createVegaRequestHandler(dependencies, context.abortSignal); const response = await vegaRequestHandler({ - timeRange: get(input, 'timeRange'), - query: get(input, 'query'), - filters: get(input, 'filters'), + timeRange: get(input, 'timeRange') as TimeRange, + query: get(input, 'query') as Query, + filters: get(input, 'filters') as any, visParams: { spec: args.spec }, }); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts index 7c4f3b3ec8843..708e8cf15f029 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import { BasicVislibParams, ValueAxis, SeriesParam } from '../../../types'; import { ChartModes, ChartTypes, InterpolationModes, Positions } from '../../../utils/collections'; @@ -67,7 +67,7 @@ const getUpdatedAxisName = ( axisPosition: ValueAxis['position'], valueAxes: BasicVislibParams['valueAxes'] ) => { - const axisName = capitalize(axisPosition) + AXIS_PREFIX; + const axisName = upperFirst(axisPosition) + AXIS_PREFIX; const nextAxisNameNumber = valueAxes.reduce(countNextAxisNumber(axisName, 'name'), 1); return `${axisName}${nextAxisNameNumber}`; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js index 87477332f76e5..4d4660371eaa4 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js +++ b/src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js @@ -30,5 +30,5 @@ export function flattenSeries(obj) { obj = obj.rows ? obj.rows : obj.columns; - return _.chain(obj).pluck('series').flattenDeep().value(); + return _.chain(obj).map('series').flattenDeep().value(); } diff --git a/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js index 5e78637ef0c02..f04d9d17eeccb 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js @@ -166,9 +166,9 @@ describe('Vislib Labels Module Test Suite', function () { seriesArr = Array.isArray(seriesLabels); rowsArr = Array.isArray(rowsLabels); uniqSeriesLabels = _.chain(rowsData.rows) - .pluck('series') + .map('series') .flattenDeep() - .pluck('label') + .map('label') .uniq() .value(); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js index 489cb81306b3d..cf98425c04ce7 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js +++ b/src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js @@ -28,5 +28,5 @@ export function uniqLabels(arr) { throw new TypeError('UniqLabelUtil expects an array of objects'); } - return _(arr).pluck('label').unique().value(); + return _(arr).map('label').uniq().value(); } diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index a2fe4d9249bd0..f7e44ed278787 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -18,7 +18,7 @@ */ import React, { BaseSyntheticEvent, KeyboardEvent, PureComponent } from 'react'; import classNames from 'classnames'; -import { compact, uniq, map, every, isUndefined } from 'lodash'; +import { compact, uniqBy, map, every, isUndefined } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; @@ -119,7 +119,7 @@ export class VisLegend extends PureComponent { getSeriesLabels = (data: any[]) => { const values = data.map((chart) => chart.series).reduce((a, b) => a.concat(b), []); - return compact(uniq(values, 'label')).map((label: any) => ({ + return compact(uniqBy(values, 'label')).map((label: any) => ({ ...label, values: [label.values[0].seriesRaw], })); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts b/src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts index 6b507862fb841..da046af83a495 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts @@ -39,7 +39,7 @@ export function getPieNames(data: any[]): string[] { }); }); - return _.uniq(names, 'label'); + return _.uniqBy(names, 'label'); } /** @@ -61,7 +61,7 @@ function getNames(data: any, columns: any): string[] { .sortBy(function (obj) { return obj.index; }) - .unique(function (d) { + .uniqBy(function (d) { return d.label; }) .value(); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js index e22105d5a086f..5324dc5318be5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js +++ b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js @@ -110,7 +110,7 @@ function getOverflow(size, pos, containers) { } function mergeOverflows(dest, src) { - _.merge(dest, src, function (a, b) { + _.mergeWith(dest, src, function (a, b) { if (a == null || b == null) return a || b; if (a < 0 && b < 0) return Math.min(a, b); return Math.max(a, b); @@ -131,7 +131,7 @@ function pickPlacement(prop, pos, overflow, prev, pref, fallback, placement) { const stash = '_' + prop; // list of directions in order of preference - const dirs = _.unique([prev[stash], pref, fallback].filter(Boolean)); + const dirs = _.uniq([prev[stash], pref, fallback].filter(Boolean)); let dir; let value; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js index 0bfcedc5e6055..bafc3199de896 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js +++ b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js @@ -218,7 +218,7 @@ Tooltip.prototype.render = function () { if (html) allContents.push({ id, html, order }); - const allHtml = _(allContents).sortBy('order').pluck('html').compact().join('\n'); + const allHtml = _(allContents).sortBy('order').map('html').compact().join('\n'); if (allHtml) { $tooltip.html(allHtml); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js index 3269f54a621d0..8b7a44d95bb3b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js +++ b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js @@ -35,9 +35,9 @@ export function flattenData(obj) { } return _(charts ? charts : [obj]) - .pluck('series') + .map('series') .flattenDeep() - .pluck('values') + .map('values') .flattenDeep() .filter(Boolean) .value(); diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts index c5fb4761eb9ee..8a1f80df9f4db 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts @@ -71,7 +71,7 @@ export function getSeries(table: Table, chart: Chart) { seriesLabel = prefix + seriesLabel; } - point.seriesId = seriesId; + (point.seriesId as string | number) = seriesId; addToSiri( seriesMap, point, diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/data.js b/src/plugins/vis_type_vislib/public/vislib/lib/data.js index 98d384f95a839..3633063966e17 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/data.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/data.js @@ -248,7 +248,7 @@ export class Data { const visData = this.getVisData(); return _.reduce( - _.pluck(visData, 'geoJson.properties'), + _.map(visData, 'geoJson.properties'), function (minMax, props) { return { min: Math.min(props.min, minMax.min), @@ -312,7 +312,7 @@ export class Data { * @returns {Array} Value objects */ flatten() { - return _(this.chartData()).pluck('series').flattenDeep().pluck('values').flattenDeep().value(); + return _(this.chartData()).map('series').flattenDeep().map('values').flattenDeep().value(); } /** @@ -383,7 +383,7 @@ export class Data { .sortBy(function (obj) { return obj.index; }) - .unique(function (d) { + .uniqBy(function (d) { return d.label; }) .value(); @@ -452,7 +452,7 @@ export class Data { }); }); - return _.uniq(names, 'label'); + return _.uniqBy(names, 'label'); } /** diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js index 37f395aab4011..4c50472b9d11a 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js @@ -18,7 +18,7 @@ */ import d3 from 'd3'; -import { get, pull, restParam, size, reduce } from 'lodash'; +import { get, pull, rest, size, reduce } from 'lodash'; import $ from 'jquery'; import { DIMMING_OPACITY_SETTING } from '../../../common'; @@ -97,7 +97,7 @@ export class Dispatch { * @param {*} [arg...] - any number of arguments that will be applied to each handler * @return {Dispatch} - this, for chaining */ - emit = restParam(function (name, args) { + emit = rest(function (name, args) { if (!this._listeners[name]) { return this; } diff --git a/src/plugins/visualizations/public/persisted_state/persisted_state.ts b/src/plugins/visualizations/public/persisted_state/persisted_state.ts index 7465f969f456f..c926c456da219 100644 --- a/src/plugins/visualizations/public/persisted_state/persisted_state.ts +++ b/src/plugins/visualizations/public/persisted_state/persisted_state.ts @@ -19,8 +19,17 @@ import { EventEmitter } from 'events'; -import { isPlainObject, cloneDeep, get, set, isEqual, isString, merge } from 'lodash'; -import toPath from 'lodash/internal/toPath'; +import { + isPlainObject, + cloneDeep, + get, + set, + isEqual, + isString, + merge, + mergeWith, + toPath, +} from 'lodash'; function prepSetParams(key: PersistedStateKey, value: any, path: PersistedStatePath) { // key must be the value, set the entire state using it @@ -150,7 +159,7 @@ export class PersistedState extends EventEmitter { while (partialPath.length > 0) { const lastKey = partialPath.splice(partialPath.length - 1, 1)[0]; const statePath = [...this._path, partialPath]; - const stateVal = statePath.length > 0 ? get(stateTree, statePath) : stateTree; + const stateVal = statePath.length > 0 ? get(stateTree, statePath as string[]) : stateTree; // if stateVal isn't an object, do nothing if (!isPlainObject(stateVal)) return; @@ -240,7 +249,7 @@ export class PersistedState extends EventEmitter { // If `mergeMethod` is provided it is invoked to produce the merged values of the // destination and source properties. // If `mergeMethod` returns `undefined` the default merging method is used - this._mergedState = merge(targetObj, sourceObj, mergeMethod); + this._mergedState = mergeWith(targetObj, sourceObj, mergeMethod); // sanity check; verify that there are actually changes if (isEqual(this._mergedState, this._defaultState)) this._changedState = {}; diff --git a/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts b/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts index 0c27c3a2c7782..60945b912e1b3 100644 --- a/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts +++ b/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts @@ -49,7 +49,7 @@ export async function findListItems({ }, acc); }, {} as { [visType: string]: VisualizationsAppExtension }); const searchOption = (field: string, ...defaults: string[]) => - _(extensions).pluck(field).concat(defaults).compact().flatten().uniq().value() as string[]; + _(extensions).map(field).concat(defaults).compact().flatten().uniq().value() as string[]; const searchOptions = { type: searchOption('docTypes', 'visualization'), searchFields: searchOption('searchFields', 'title^3', 'description'), diff --git a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx index 47757593958d6..dc6ac4919a4c4 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; import React, { ChangeEvent } from 'react'; import { @@ -201,7 +201,7 @@ class TypeSelection extends React.Component { diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 27fe722019a27..74881b9d99ae8 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -65,7 +65,7 @@ const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { // [TSVB] Migrate percentile-rank aggregation (value -> values) const migratePercentileRankAggregation: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); let visState; if (visStateJSON) { @@ -101,7 +101,7 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn = (doc) // [TSVB] Remove stale opperator key const migrateOperatorKeyTypo: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); let visState; if (visStateJSON) { @@ -137,7 +137,7 @@ const migrateOperatorKeyTypo: SavedObjectMigrationFn = (doc) => { * @see https://github.com/elastic/kibana/pull/58462/files#diff-ae69fe15b20a5099d038e9bbe2ed3849 **/ const migrateSplitByChartRow: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); let visState: any; if (visStateJSON) { @@ -177,7 +177,7 @@ const migrateSplitByChartRow: SavedObjectMigrationFn = (doc) => { // Migrate date histogram aggregation (remove customInterval) const migrateDateHistogramAggregation: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); let visState; if (visStateJSON) { @@ -219,7 +219,7 @@ const migrateDateHistogramAggregation: SavedObjectMigrationFn = (doc) }; const removeDateHistogramTimeZones: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { let visState; try { @@ -251,7 +251,7 @@ const removeDateHistogramTimeZones: SavedObjectMigrationFn = (doc) => // migrate gauge verticalSplit to alignment // https://github.com/elastic/kibana/issues/34636 const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { try { @@ -289,7 +289,7 @@ const transformFilterStringToQueryObject: SavedObjectMigrationFn = (do // Migrate filters // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly const newDoc = cloneDeep(doc); - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { let visState; try { @@ -298,7 +298,7 @@ const transformFilterStringToQueryObject: SavedObjectMigrationFn = (do // let it go, the data is invalid and we'll leave it as is } if (visState) { - const visType = get(visState, 'params.type'); + const visType = get(visState, 'params.type'); const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; if (tsvbTypes.indexOf(visType) === -1) { // skip @@ -373,7 +373,7 @@ const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn // Migrate split_filters in TSVB objects that weren't migrated in 7.3 // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly const newDoc = cloneDeep(doc); - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { let visState; try { @@ -382,7 +382,7 @@ const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn // let it go, the data is invalid and we'll leave it as is } if (visState) { - const visType = get(visState, 'params.type'); + const visType = get(visState, 'params.type'); const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; if (tsvbTypes.indexOf(visType) === -1) { // skip @@ -415,7 +415,7 @@ const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn }; const migrateFiltersAggQuery: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { try { @@ -447,7 +447,7 @@ const migrateFiltersAggQuery: SavedObjectMigrationFn = (doc) => { }; const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); let visState; if (visStateJSON) { @@ -495,7 +495,7 @@ const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => }; const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { try { @@ -533,7 +533,7 @@ const addDocReferences: SavedObjectMigrationFn = (doc) => ({ }); const migrateSavedSearch: SavedObjectMigrationFn = (doc) => { - const savedSearchId = get(doc, 'attributes.savedSearchId'); + const savedSearchId = get(doc, 'attributes.savedSearchId'); if (savedSearchId && doc.references) { doc.references.push({ @@ -550,7 +550,7 @@ const migrateSavedSearch: SavedObjectMigrationFn = (doc) => { }; const migrateControls: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { let visState; @@ -617,7 +617,7 @@ const migrateTableSplits: SavedObjectMigrationFn = (doc) => { }; const migrateMatchAllQuery: SavedObjectMigrationFn = (doc) => { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { let searchSource: any; @@ -651,7 +651,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = (doc) => { // [TSVB] Default color palette is changing, keep the default for older viz const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = (doc) => { - const visStateJSON = get(doc, 'attributes.visState'); + const visStateJSON = get(doc, 'attributes.visState'); let visState; if (visStateJSON) { @@ -693,30 +693,24 @@ export const visualizationSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow>( - migrateMatchAllQuery, - removeDateHistogramTimeZones - ), - '7.0.0': flow>( + '6.7.2': flow(migrateMatchAllQuery, removeDateHistogramTimeZones), + '7.0.0': flow( addDocReferences, migrateIndexPattern, migrateSavedSearch, migrateControls, migrateTableSplits ), - '7.0.1': flow>(removeDateHistogramTimeZones), - '7.2.0': flow>( - migratePercentileRankAggregation, - migrateDateHistogramAggregation - ), - '7.3.0': flow>( + '7.0.1': flow(removeDateHistogramTimeZones), + '7.2.0': flow(migratePercentileRankAggregation, migrateDateHistogramAggregation), + '7.3.0': flow( migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject, migrateFiltersAggQuery, replaceMovAvgToMovFn ), - '7.3.1': flow>(migrateFiltersAggQueryStringQueries), - '7.4.2': flow>(transformSplitFiltersStringToQueryObject), - '7.7.0': flow>(migrateOperatorKeyTypo, migrateSplitByChartRow), - '7.8.0': flow>(migrateTsvbDefaultColorPalettes), + '7.3.1': flow(migrateFiltersAggQueryStringQueries), + '7.4.2': flow(transformSplitFiltersStringToQueryObject), + '7.7.0': flow(migrateOperatorKeyTypo, migrateSplitByChartRow), + '7.8.0': flow(migrateTsvbDefaultColorPalettes), }; diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts index 1e05c48ba7daf..52b7e3ede298b 100644 --- a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts +++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isFunction, omit, union } from 'lodash'; +import { isFunction, omitBy, union } from 'lodash'; import { migrateAppState } from './migrate_app_state'; import { @@ -35,9 +35,9 @@ interface Arguments { } function toObject(state: PureVisState): PureVisState { - return omit(state, (value, key: string) => { + return omitBy(state, (value, key: string) => { return key.charAt(0) === '$' || key.charAt(0) === '_' || isFunction(value); - }); + }) as PureVisState; } export function createVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { diff --git a/src/plugins/visualize/public/application/utils/migrate_app_state.ts b/src/plugins/visualize/public/application/utils/migrate_app_state.ts index f5f1a1785bbdf..94eba5a6d7ce2 100644 --- a/src/plugins/visualize/public/application/utils/migrate_app_state.ts +++ b/src/plugins/visualize/public/application/utils/migrate_app_state.ts @@ -36,7 +36,7 @@ export function migrateAppState(appState: VisualizeAppState) { return appState; } - const visAggs: any = get(appState, 'vis.aggs'); + const visAggs: any = get(appState, 'vis.aggs'); if (visAggs) { let splitCount = 0; diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/harden/child_process.js similarity index 97% rename from src/setup_node_env/patches/child_process.js rename to src/setup_node_env/harden/child_process.js index fb857b2092ee0..6b1ba779605c0 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/harden/child_process.js @@ -16,12 +16,13 @@ * specific language governing permissions and limitations * under the License. */ +var hook = require('require-in-the-middle'); // Ensure, when spawning a new child process, that the `options` and the // `options.env` object passed to the child process function doesn't inherit // from `Object.prototype`. This protects against similar RCE vulnerabilities // as described in CVE-2019-7609 -module.exports = function (cp) { +hook(['child_process'], function (cp) { // The `exec` function is currently just a wrapper around `execFile`. So for // now there's no need to patch it. If this changes in the future, our tests // will fail and we can uncomment the line below. @@ -36,7 +37,7 @@ module.exports = function (cp) { cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptions(true) }); return cp; -}; +}); function patchOptions(hasArgs) { return function apply(target, thisArg, args) { diff --git a/typings/lodash.topath/index.d.ts b/src/setup_node_env/harden/index.js similarity index 87% rename from typings/lodash.topath/index.d.ts rename to src/setup_node_env/harden/index.js index 3630a13f72c28..25cb3bcd78ffb 100644 --- a/typings/lodash.topath/index.d.ts +++ b/src/setup_node_env/harden/index.js @@ -17,7 +17,5 @@ * under the License. */ -declare module 'lodash/internal/toPath' { - function toPath(value: string | string[]): string[]; - export = toPath; -} +require('./child_process'); +require('./lodash_template'); diff --git a/src/setup_node_env/harden/lodash_template.js b/src/setup_node_env/harden/lodash_template.js new file mode 100644 index 0000000000000..2add624f9f326 --- /dev/null +++ b/src/setup_node_env/harden/lodash_template.js @@ -0,0 +1,68 @@ +/* + * 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. + */ +var hook = require('require-in-the-middle'); +var isIterateeCall = require('lodash/_isIterateeCall'); + +hook(['lodash'], function (lodash) { + lodash.template = createProxy(lodash.template); + return lodash; +}); + +hook(['lodash/template'], function (template) { + return createProxy(template); +}); + +hook(['lodash/fp'], function (fp) { + fp.template = createFpProxy(fp.template); + return fp; +}); + +hook(['lodash/fp/template'], function (template) { + return createFpProxy(template); +}); + +function createProxy(template) { + return new Proxy(template, { + apply: function (target, thisArg, args) { + if (args.length === 1 || isIterateeCall(args)) { + return target.apply(thisArg, [args[0], { sourceURL: '' }]); + } + + var options = Object.assign({}, args[1]); + options.sourceURL = (options.sourceURL + '').replace(/\s/g, ' '); + var newArgs = args.slice(0); // copy + newArgs.splice(1, 1, options); // replace options in the copy + return target.apply(thisArg, newArgs); + }, + }); +} + +function createFpProxy(template) { + // we have to do the require here, so that we get the patched version + var _ = require('lodash'); + return new Proxy(template, { + apply: function (target, thisArg, args) { + // per https://github.com/lodash/lodash/wiki/FP-Guide + // > Iteratee arguments are capped to avoid gotchas with variadic iteratees. + // this means that we can't specify the options in the second argument to fp.template because it's ignored. + // Instead, we're going to use the non-FP _.template with only the first argument which has already been patched + return _.template(args[0]); + }, + }); +} diff --git a/src/test_utils/get_url.js b/src/test_utils/get_url.js index fbe16e798fff9..182cb8e6e118d 100644 --- a/src/test_utils/get_url.js +++ b/src/test_utils/get_url.js @@ -44,7 +44,7 @@ export default function getUrl(config, app) { } getUrl.noAuth = function getUrlNoAuth(config, app) { - config = _.pick(config, function (val, param) { + config = _.pickBy(config, function (val, param) { return param !== 'auth'; }); return getUrl(config, app); diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index 12f7eb5a0a043..6a20261421b5b 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -217,7 +217,7 @@ export function createTestServers({ if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } - const license = get<'oss' | 'basic' | 'gold' | 'trial'>(settings, 'es.license', 'oss'); + const license = get(settings, 'es.license', 'oss'); const usersToBeAdded = get(settings, 'users', []); if (usersToBeAdded.length > 0) { if (license !== 'trial') { diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js index d0ff4cc06c57e..9ea3cf087be90 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.js @@ -293,7 +293,7 @@ export default ({ getService }) => { // It only created the original and the dest assert.deepEqual( - _.pluck( + _.map( await callCluster('cat.indices', { index: '.migration-c*', format: 'json' }), 'index' ).sort(), diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index c69111be6972b..03db3a2b108f2 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); @@ -50,12 +50,12 @@ export default function ({ getService, getPageObjects }) { await PageObjects.savedObjects.clickImportDone(); // get all the elements in the table, and index them by the 'title' visible text field - const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); + const elements = keyBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); log.debug("check that 'Log Agents' is in table as a visualization"); expect(elements['Log Agents'].objectType).to.eql('visualization'); await elements['logstash-*'].relationshipsElement.click(); - const flyout = indexBy(await PageObjects.savedObjects.getRelationshipFlyout(), 'title'); + const flyout = keyBy(await PageObjects.savedObjects.getRelationshipFlyout(), 'title'); log.debug( "check that 'Shared-Item Visualization AreaChart' shows 'logstash-*' as it's Parent" ); @@ -150,7 +150,7 @@ export default function ({ getService, getPageObjects }) { }); it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () { - const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); + const elements = keyBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); await PageObjects.savedObjects.clickDelete(); @@ -182,7 +182,7 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects with index patterns when index patterns does not exists', async () => { // First, we need to delete the index pattern - const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); + const elements = keyBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); await PageObjects.savedObjects.clickDelete(); @@ -321,7 +321,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.savedObjects.clickImportDone(); // Second, we need to delete the index pattern - const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); + const elements = keyBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); await PageObjects.savedObjects.clickDelete(); @@ -353,7 +353,7 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects with index patterns when index patterns does not exists', async () => { // First, we need to delete the index pattern - const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); + const elements = keyBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); await PageObjects.savedObjects.clickDelete(); diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index d6a4fc91481de..2d35551b04808 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -17,7 +17,7 @@ * under the License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeepWith } from 'lodash'; import { Key, Origin } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed import { LegacyActionSequence } from 'selenium-webdriver/lib/actions'; @@ -471,7 +471,7 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { ): Promise { return await driver.executeScript( fn, - ...cloneDeep(args, (arg) => { + ...cloneDeepWith(args, (arg) => { if (arg instanceof WebElementWrapper) { return arg._webElement; } @@ -501,7 +501,7 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { ): Promise { return await driver.executeAsyncScript( fn, - ...cloneDeep(args, (arg) => { + ...cloneDeepWith(args, (arg) => { if (arg instanceof WebElementWrapper) { return arg._webElement; } diff --git a/test/harden/lodash_template.js b/test/harden/lodash_template.js new file mode 100644 index 0000000000000..170e3a8fba43e --- /dev/null +++ b/test/harden/lodash_template.js @@ -0,0 +1,181 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../../src/setup_node_env'); +const _ = require('lodash'); +const template = require('lodash/template'); +const fp = require('lodash/fp'); +const fpTemplate = require('lodash/fp/template'); +const test = require('tape'); + +Object.prototype.sourceURL = '\u2028\u2029\n;global.whoops=true'; // eslint-disable-line no-extend-native + +test.onFinish(() => { + delete Object.prototype.sourceURL; +}); + +test('test setup ok', (t) => { + t.equal({}.sourceURL, '\u2028\u2029\n;global.whoops=true'); + t.end(); +}); + +[_.template, template].forEach((fn) => { + test(`_.template('<%= foo %>')`, (t) => { + const output = fn('<%= foo %>')({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); + }); + + test(`_.template('<%= foo %>', {})`, (t) => { + const output = fn('<%= foo %>', Object.freeze({}))({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); + }); + + test(`_.template('<%= data.foo %>', { variable: 'data' })`, (t) => { + const output = fn('<%= data.foo %>', Object.freeze({ variable: 'data' }))({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); + }); + + test(`_.template('<%= foo %>', { sourceURL: '/foo/bar' })`, (t) => { + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + const template = fn('<% throw new Error() %>', Object.freeze({ sourceURL: '/foo/bar' })); + t.plan(2); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.equal(path, '/foo/bar'); + t.equal(global.whoops, undefined); + } + }); + + test(`_.template('<%= foo %>', { sourceURL: '\\u2028\\u2029\\nglobal.whoops=true' })`, (t) => { + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + const template = fn( + '<% throw new Error() %>', + Object.freeze({ sourceURL: '\u2028\u2029\nglobal.whoops=true' }) + ); + t.plan(2); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.equal(path, 'global.whoops=true'); + t.equal(global.whoops, undefined); + } + }); + + test(`_.template used as an iteratee call(`, (t) => { + const templateStrArr = ['<%= data.foo %>', 'example <%= data.foo %>']; + const output = _.map(templateStrArr, fn); + + t.equal(output[0]({ data: { foo: 'bar' } }), 'bar'); + t.equal(output[1]({ data: { foo: 'bar' } }), 'example bar'); + t.equal(global.whoops, undefined); + t.end(); + }); +}); + +[fp.template, fpTemplate].forEach((fn) => { + test(`fp.template('<%= foo %>')`, (t) => { + const output = fn('<%= foo %>')({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); + }); + + test(`fp.template('<%= foo %>', {})`, (t) => { + // fp.template ignores the second argument, this is negligible in this situation since options is an empty object + const output = fn('<%= foo %>', Object.freeze({}))({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); + }); + + test(`fp.template('<%= data.foo %>', { variable: 'data' })`, (t) => { + // fp.template ignores the second argument, this causes an error to be thrown + t.plan(2); + try { + fn('<%= data.foo %>', Object.freeze({ variable: 'data' }))({ foo: 'bar' }); + } catch (err) { + t.equal(err.message, 'data is not defined'); + t.equal(global.whoops, undefined); + } + }); + + test(`fp.template('<%= foo %>', { sourceURL: '/foo/bar' })`, (t) => { + // fp.template ignores the second argument, the sourceURL is ignored + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + // our patching to hard-code the sourceURL and use non-FP _.template does slightly alter the stack-traces but it's negligible + const template = fn('<% throw new Error() %>', Object.freeze({ sourceURL: '/foo/bar' })); + t.plan(3); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.match(path, /^eval at /); + t.doesNotMatch(path, /\/foo\/bar/); + t.equal(global.whoops, undefined); + } + }); + + test(`fp.template('<%= foo %>', { sourceURL: '\\u2028\\u2029\\nglobal.whoops=true' })`, (t) => { + // fp.template ignores the second argument, the sourceURL is ignored + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + // our patching to hard-code the sourceURL and use non-FP _.template does slightly alter the stack-traces but it's negligible + const template = fn( + '<% throw new Error() %>', + Object.freeze({ sourceURL: '\u2028\u2029\nglobal.whoops=true' }) + ); + t.plan(3); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.match(path, /^eval at /); + t.doesNotMatch(path, /\/foo\/bar/); + t.equal(global.whoops, undefined); + } + }); + + test(`fp.template used as an iteratee call(`, (t) => { + const templateStrArr = ['<%= data.foo %>', 'example <%= data.foo %>']; + const output = fp.map(fn)(templateStrArr); + + t.equal(output[0]({ data: { foo: 'bar' } }), 'bar'); + t.equal(output[1]({ data: { foo: 'bar' } }), 'example bar'); + t.equal(global.whoops, undefined); + t.end(); + }); +}); + +function parsePathFromStack(stack) { + const lines = stack.split('\n'); + // the frame starts at the second line + const frame = lines[1]; + + // the path is in parathensis, and ends with a colon before the line/column numbers + const [, path] = /\(([^:]+)/.exec(frame); + return path; +} diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_visual_regression.sh index a32782deec65b..17345d4301882 100755 --- a/test/scripts/jenkins_visual_regression.sh +++ b/test/scripts/jenkins_visual_regression.sh @@ -11,7 +11,7 @@ mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 echo " -> running visual regression tests from kibana directory" -yarn percy exec -t 500 -- -- \ +yarn percy exec -t 10000 -- -- \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$installDir" \ diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index b67c1c9060a6e..36bf3409a5421 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -13,7 +13,7 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1 echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" -yarn percy exec -t 500 -- -- \ +yarn percy exec -t 10000 -- -- \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$installDir" \ diff --git a/test/tsconfig.json b/test/tsconfig.json index a270144bd49fe..87e79b295315f 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -14,7 +14,6 @@ "include": [ "**/*.ts", "**/*.tsx", - "../typings/lodash.topath/*.ts", "../typings/elastic__node_crypto.d.ts", "typings/**/*" ], diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts index 3a71c3aa9d3d6..e35ef41420dd6 100644 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ b/test/visual_regression/services/visual_testing/visual_testing.ts @@ -19,7 +19,6 @@ import { postSnapshot } from '@percy/agent/dist/utils/sdk-utils'; import { Test } from 'mocha'; -import _ from 'lodash'; import testSubjSelector from '@kbn/test-subj-selector'; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 0600ed8e3fbf6..7c495ad605f6d 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -38,7 +38,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { if (!response.found) { return null; } - const beat = _get(response, '_source.beat'); + const beat = _get(response, '_source.beat') as CMBeat; beat.tags = beat.tags || []; return beat; } @@ -101,7 +101,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { const response = await this.database.search(user, params); - const beats = _get(response, 'hits.hits', []); + const beats = _get(response, 'hits.hits', []) as CMBeat[]; if (beats.length === 0) { return []; @@ -127,14 +127,12 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { const response = await this.database.search(user, params); - const beats = _get(response, 'hits.hits', []); + const beats = _get(response, 'hits.hits', []) as CMBeat[]; if (beats.length === 0) { return null; } - return omit(_get(formatWithTags(beats[0]), '_source.beat'), [ - 'access_token', - ]); + return omit(_get(formatWithTags(beats[0]), '_source.beat'), ['access_token']) as CMBeat; } public async getAll(user: FrameworkUser, ESQuery?: any) { @@ -171,7 +169,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { if (!response) { return []; } - const beats = _get(response, 'hits.hits', []); + const beats = _get(response, 'hits.hits', []) as any; return beats.map((beat: any) => formatWithTags(omit(beat._source.beat as CMBeat, ['access_token']) as CMBeat) @@ -202,7 +200,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { index: INDEX_NAMES.BEATS, refresh: 'wait_for', }); - return _get(response, 'items', []).map((item: any, resultIdx: number) => ({ + return (_get(response, 'items', []) as any).map((item: any, resultIdx: number) => ({ idxInRequest: removals[resultIdx].idxInRequest, result: item.update.result, status: item.update.status, @@ -237,7 +235,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { refresh: 'wait_for', }); // console.log(response.items[0].update.error); - return _get(response, 'items', []).map((item: any, resultIdx: any) => ({ + return (_get(response, 'items', []) as any).map((item: any, resultIdx: any) => ({ idxInRequest: assignments[resultIdx].idxInRequest, result: item.update.result, status: item.update.status, diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts index 2bc6f18756447..ec559c3ee479c 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts @@ -35,7 +35,7 @@ export class ElasticsearchConfigurationBlockAdapter implements ConfigurationBloc }; const response = await this.database.search(user, params); - const configs = get(response, 'hits.hits', []); + const configs = get(response, 'hits.hits', []); return configs.map((tag: any) => ({ ...tag._source.tag, config: JSON.parse(tag._source.tag) })); } @@ -71,7 +71,7 @@ export class ElasticsearchConfigurationBlockAdapter implements ConfigurationBloc } else { response = await this.database.search(user, params); } - const configs = get(response, 'hits.hits', []); + const configs = get(response, 'hits.hits', []); return { blocks: configs.map((block: any) => ({ diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts index 4e032001809f2..b5be3cfa99e5d 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -43,7 +43,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { }; } const response = await this.database.search(user, params); - const tags = get(response, 'hits.hits', []); + const tags = get(response, 'hits.hits', []) as any; return tags.map((tag: any) => ({ hasConfigurationBlocksTypes: [], ...tag._source.tag })); } @@ -63,7 +63,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { const beatsResponse = await this.database.search(user, params); - const beats = get(beatsResponse, 'hits.hits', []).map( + const beats = (get(beatsResponse, 'hits.hits', []) as BeatTag[]).map( (beat: any) => beat._source.beat ); @@ -142,7 +142,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { }; const response = await this.database.index(user, params); - return get(response, 'result'); + return get(response, 'result') as string; } public async getWithoutConfigTypes( @@ -172,7 +172,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { size: 10000, }; const response = await this.database.search(user, params); - const tags = get(response, 'hits.hits', []); + const tags = get(response, 'hits.hits', []) as any; return tags.map((tag: any) => ({ hasConfigurationBlocksTypes: [], ...tag._source.tag })); } diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts index 4987e4dbd4e0a..6c5125ea4e0eb 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -34,10 +34,10 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { const response = await this.database.get(user, params); - const tokenDetails = get(response, '_source.enrollment_token', { + const tokenDetails = get(response, '_source.enrollment_token', { expires_on: '0', token: null, - }); + }) as TokenEnrollmentData; // Elasticsearch might return fast if the token is not found. OR it might return fast // if the token *is* found. Either way, an attacker could using a timing attack to figure diff --git a/x-pack/package.json b/x-pack/package.json index 0a8bc6f1e6f58..b721cb2fc563a 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -81,7 +81,7 @@ "@types/jsdom": "^16.2.3", "@types/json-stable-stringify": "^1.0.32", "@types/jsonwebtoken": "^7.2.8", - "@types/lodash": "^3.10.1", + "@types/lodash": "^4.14.155", "@types/mapbox-gl": "^1.9.1", "@types/memoize-one": "^4.1.0", "@types/mime": "^2.0.1", @@ -281,11 +281,7 @@ "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", - "lodash": "npm:@elastic/lodash@3.10.1-kibana4", - "lodash.keyby": "^4.6.0", - "lodash.mean": "^4.1.0", - "lodash.topath": "^4.5.2", - "lodash.uniqby": "^4.7.0", + "lodash": "^4.17.15", "lz-string": "^1.4.4", "mapbox-gl": "^1.10.0", "mapbox-gl-draw-rectangle-mode": "^1.0.4", diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 605676cee363d..494f2f38e8bff 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -71,6 +71,7 @@ Table of Contents - [`params`](#params-7) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) - [Command Line Utility](#command-line-utility) +- [Developing New Action Types](#developing-new-action-types) ## Terminology @@ -606,3 +607,39 @@ $ kbn-action create .slack "post to slack" '{"webhookUrl": "https://hooks.slack. "version": "WzMsMV0=" } ``` + +# Developing New Action Types + +When creating a new action type, your plugin will eventually call `server.plugins.actions.setup.registerType()` to register the type with the actions plugin, but there are some additional things to think about about and implement. + +Consider working with the alerting team on early structure /design feedback of new actions, especially as the APIs and infrastructure are still under development. + +## licensing + +Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. + +## plugin location + +Currently actions that are licensed as "basic" **MUST** be implemented in the actions plugin, other actions can be implemented in any other plugin that pre-reqs the actions plugin. If the new action is generic across the stack, it probably belongs in the actions plugin, but if your action is very specific to a plugin/solution, it might be easiest to implement it in the plugin/solution. Keep in mind that if Kibana is run without the plugin being enabled, any actions defined in that plugin will not run, nor will those actions be available via APIs or UI. + +Actions that take URLs or hostnames should check that those values are whitelisted. The whitelisting utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). + +## documentation + +You should also create some asciidoc for the new action type. An entry should be made in the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc) which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). + +## tests + +The action type should have both jest tests and functional tests. For functional tests, if your action interacts with a 3rd party service via HTTP, you may be able to create a simulator for your service, to test with. See the existing functional test servers in the directory [`x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server`](../../test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server) + +## action type config and secrets + +Action types must define `config` and `secrets` which are used to create connectors. This data should be described with `@kbn/config-schema` object schemas, and you **MUST NOT** use `schema.maybe()` to define properties. + +This is due to the fact that the structures are persisted in saved objects, which performs partial updates on the persisted data. If a property value is already persisted, but an update either doesn't include the property, or sets it to `undefined`, the persisted value will not be changed. Beyond this being a semantic error in general, it also ends up invalidating the encryption used to save secrets, and will render the secrets will not be able to be unencrypted later. + +Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `schema.maybe()` except that when passed an `undefined` value, the object returned from the validation will be set to `null`. The resulting type will be `property-type | null`, whereas with `schema.maybe()` it would be `property-type | undefined`. + +## user interface + +In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index dd8d971b7df44..2d81c2bf4e15f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -26,7 +26,7 @@ import { ExecutorSubActionPushParams, } from './types'; -import { transformers, Transformer } from './transformers'; +import { transformers } from './transformers'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; @@ -205,7 +205,7 @@ export const transformFields = ({ currentIncident, }: TransformFieldsArgs): Record => { return fields.reduce((prev, cur) => { - const transform = flow(...cur.pipes.map((p) => transformers[p])); + const transform = flow(...cur.pipes.map((p) => transformers[p])); return { ...prev, [cur.key]: transform({ @@ -228,7 +228,7 @@ export const transformFields = ({ export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { return comments.map((c) => ({ ...c, - comment: flow(...pipes.map((p) => transformers[p]))({ + comment: flow(...pipes.map((p) => transformers[p]))({ value: c.comment, date: c.updatedAt ?? c.createdAt, user: diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts index 5dff062922200..aa546e08ea1ba 100644 --- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts +++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts @@ -20,7 +20,7 @@ export function createActionsUsageCollector( try { const doc = await getLatestTaskState(await taskManager); // get the accumulated state from the recurring task - const state: ActionsUsage = get(doc, 'state'); + const state: ActionsUsage = get(doc, 'state') as ActionsUsage; return { ...state, diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 6b091a5a4503b..e8e6f82f13882 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { omit, isEqual, pluck } from 'lodash'; +import { omit, isEqual, map } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, @@ -647,7 +647,7 @@ export class AlertsClient { private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void { const { actionGroups: alertTypeActionGroups } = alertType; const usedAlertActionGroups = actions.map((action) => action.group); - const availableAlertTypeActionGroups = new Set(pluck(alertTypeActionGroups, 'id')); + const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); const invalidActionGroups = usedAlertActionGroups.filter( (group) => !availableAlertTypeActionGroups.has(group) ); diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 8d859a570ba91..e1e1568d2f13c 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pluck } from 'lodash'; +import { map } from 'lodash'; import { AlertAction, State, Context, AlertType } from '../types'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; @@ -46,7 +46,7 @@ export function createExecutionHandler({ eventLogger, request, }: CreateExecutionHandlerOptions) { - const alertTypeActionGroups = new Set(pluck(alertType.actionGroups, 'id')); + const alertTypeActionGroups = new Set(map(alertType.actionGroups, 'id')); return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => { if (!alertTypeActionGroups.has(actionGroup)) { logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`); 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 3512ab16a3712..3c66b57bb9416 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick, mapValues, omit, without } from 'lodash'; +import { pickBy, mapValues, omit, without } from 'lodash'; import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../task_manager/server'; @@ -18,12 +18,11 @@ import { IntervalSchedule, Services, AlertInfoParams, - RawAlertInstance, AlertTaskState, + RawAlertInstance, } from '../types'; import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type'; import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; -import { AlertInstances } from '../alert_instance/alert_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; @@ -167,7 +166,7 @@ export class TaskRunner { } = this.taskInstance; const namespace = this.context.spaceIdToNamespace(spaceId); - const alertInstances = mapValues( + const alertInstances = mapValues, AlertInstance>( alertRawInstances, (rawAlertInstance) => new AlertInstance(rawAlertInstance) ); @@ -227,9 +226,8 @@ export class TaskRunner { eventLogger.logEvent(event); // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object - const instancesWithScheduledActions = pick( - alertInstances, - (alertInstance: AlertInstance) => alertInstance.hasScheduledActions() + const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => + alertInstance.hasScheduledActions() ); const currentAlertInstanceIds = Object.keys(instancesWithScheduledActions); generateNewAndResolvedInstanceEvents({ @@ -242,10 +240,7 @@ export class TaskRunner { }); if (!muteAll) { - const enabledAlertInstances = omit( - instancesWithScheduledActions, - ...mutedInstanceIds - ); + const enabledAlertInstances = omit(instancesWithScheduledActions, ...mutedInstanceIds); await Promise.all( Object.entries(enabledAlertInstances) @@ -260,7 +255,7 @@ export class TaskRunner { return { alertTypeState: updatedAlertTypeState || undefined, - alertInstances: mapValues( + alertInstances: mapValues, RawAlertInstance>( instancesWithScheduledActions, (alertInstance) => alertInstance.toRaw() ), diff --git a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts index 64f846d13c0bf..fa4a0e40ddee5 100644 --- a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts @@ -5,7 +5,7 @@ */ import Mustache from 'mustache'; -import { isString, cloneDeep } from 'lodash'; +import { isString, cloneDeepWith } from 'lodash'; import { AlertActionParams, State, Context } from '../types'; interface TransformActionParamsOptions { @@ -29,7 +29,7 @@ export function transformActionParams({ actionParams, state, }: TransformActionParamsOptions): AlertActionParams { - const result = cloneDeep(actionParams, (value: unknown) => { + const result = cloneDeepWith(actionParams, (value: unknown) => { if (!isString(value)) return; // when the list of variables we pass in here changes, diff --git a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts index 7491508ee0745..64d3ad54a2318 100644 --- a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts @@ -20,7 +20,7 @@ export function createAlertsUsageCollector( try { const doc = await getLatestTaskState(await taskManager); // get the accumulated state from the recurring task - const state: AlertsUsage = get(doc, 'state'); + const state: AlertsUsage = get(doc, 'state') as AlertsUsage; return { ...state, diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts index 80a3471e9c30d..809caeeaf6088 100644 --- a/x-pack/plugins/apm/common/projections/services.ts +++ b/x-pack/plugins/apm/common/projections/services.ts @@ -16,25 +16,37 @@ import { rangeFilter } from '../utils/range_filter'; export function getServicesProjection({ setup, + noEvents, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + noEvents?: boolean; }) { const { start, end, uiFiltersES, indices } = setup; return { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + ...(noEvents + ? {} + : { + index: [ + indices['apm_oss.metricsIndices'], + indices['apm_oss.errorIndices'], + indices['apm_oss.transactionIndices'], + ], + }), body: { size: 0, query: { bool: { filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, + ...(noEvents + ? [] + : [ + { + terms: { + [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'], + }, + }, + ]), { range: rangeFilter(start, end) }, ...uiFiltersES, ], diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts index f3ae0752b908e..9dc1c815bf169 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/common/projections/util/merge_projection/index.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 { merge, isPlainObject, cloneDeep } from 'lodash'; +import { mergeWith, isPlainObject, cloneDeep } from 'lodash'; import { DeepPartial } from 'utility-types'; import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; import { @@ -35,7 +35,7 @@ export function mergeProjection< T extends Projection, U extends SourceProjection >(target: T, source: U): DeepMerge { - return merge({}, cloneDeep(target), source, (a, b) => { + return mergeWith({}, cloneDeep(target), source, (a, b) => { if (isPlainObject(a) && isPlainObject(b)) { return undefined; } diff --git a/x-pack/plugins/apm/common/utils/array_union_to_callable.ts b/x-pack/plugins/apm/common/utils/array_union_to_callable.ts new file mode 100644 index 0000000000000..23ea86006b888 --- /dev/null +++ b/x-pack/plugins/apm/common/utils/array_union_to_callable.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 { ValuesType } from 'utility-types'; + +// work around a TypeScript limitation described in https://stackoverflow.com/posts/49511416 + +export const arrayUnionToCallable = ( + array: T +): Array> => { + return array; +}; diff --git a/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts b/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts new file mode 100644 index 0000000000000..458d21bfea58f --- /dev/null +++ b/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { joinByKey } from './'; + +describe('joinByKey', () => { + it('joins by a string key', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + avg: 10, + }, + { + serviceName: 'opbeans-node', + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + }, + { + serviceName: 'opbeans-java', + p95: 18, + }, + ], + 'serviceName' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + serviceName: 'opbeans-node', + avg: 10, + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + p95: 18, + }, + ]); + }); + + it('joins by a record key', () => { + const joined = joinByKey( + [ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + }, + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + p95: 18, + }, + ], + 'key' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + p95: 18, + }, + ]); + }); +}); diff --git a/x-pack/plugins/apm/common/utils/join_by_key/index.ts b/x-pack/plugins/apm/common/utils/join_by_key/index.ts new file mode 100644 index 0000000000000..b49f536400514 --- /dev/null +++ b/x-pack/plugins/apm/common/utils/join_by_key/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { UnionToIntersection, ValuesType } from 'utility-types'; +import { isEqual } from 'lodash'; + +/** + * Joins a list of records by a given key. Key can be any type of value, from + * strings to plain objects, as long as it is present in all records. `isEqual` + * is used for comparing keys. + * + * UnionToIntersection is needed to get all keys of union types, see below for + * example. + * + const agentNames = [{ serviceName: '', agentName: '' }]; + const transactionRates = [{ serviceName: '', transactionsPerMinute: 1 }]; + const flattened = joinByKey( + [...agentNames, ...transactionRates], + 'serviceName' + ); +*/ + +type JoinedReturnType< + T extends Record, + U extends UnionToIntersection, + V extends keyof T & keyof U +> = Array & Record>; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends keyof T & keyof U +>(items: T[], key: V): JoinedReturnType { + return items.reduce>((prev, current) => { + let item = prev.find((prevItem) => isEqual(prevItem[key], current[key])); + + if (!item) { + item = { ...current } as ValuesType>; + prev.push(item); + } else { + Object.assign(item, current); + } + + return prev; + }, []); +} diff --git a/x-pack/plugins/apm/common/utils/left_join.ts b/x-pack/plugins/apm/common/utils/left_join.ts deleted file mode 100644 index f3c4e48df755b..0000000000000 --- a/x-pack/plugins/apm/common/utils/left_join.ts +++ /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 { Assign, Omit } from 'utility-types'; - -export function leftJoin< - TL extends object, - K extends keyof TL, - TR extends Pick ->(leftRecords: TL[], matchKey: K, rightRecords: TR[]) { - const rightLookup = new Map( - rightRecords.map((record) => [record[matchKey], record]) - ); - return leftRecords.map((record) => { - const matchProp = (record[matchKey] as unknown) as TR[K]; - const matchingRightRecord = rightLookup.get(matchProp); - return { ...record, ...matchingRightRecord }; - }) as Array>>>; -} diff --git a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature index bc807d596a272..c98e3f81b2bc6 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature @@ -37,3 +37,6 @@ Feature: RUM Dashboard When the user selected the breakdown Then breakdown series should appear in chart + Scenario: Service name filter + When a user changes the selected service name + Then it displays relevant client metrics diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index acccd86f3e4d7..ac09e575a46ae 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -9,17 +9,17 @@ module.exports = { }, "RUM Dashboard": { "Client metrics": { - "1": "62 ", - "2": "0.07 sec", + "1": "55 ", + "2": "0.08 sec", "3": "0.01 sec" }, "Rum page filters (example #1)": { - "1": "15 ", - "2": "0.07 sec", + "1": "8 ", + "2": "0.08 sec", "3": "0.01 sec" }, "Rum page filters (example #2)": { - "1": "35 ", + "1": "28 ", "2": "0.07 sec", "3": "0.01 sec" }, @@ -31,6 +31,11 @@ module.exports = { }, "Page load distribution chart legends": { "1": "Overall" + }, + "Service name filter": { + "1": "7 ", + "2": "0.07 sec", + "3": "0.01 sec" } } } diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts index 809b22490abd6..89dc3437c3e69 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts @@ -18,7 +18,9 @@ Given(`a user click page load breakdown filter`, () => { }); When(`the user selected the breakdown`, () => { - cy.get('[data-cy=filter-breakdown-item_Browser]').click(); + cy.get('[data-cy=filter-breakdown-item_Browser]', { + timeout: DEFAULT_TIMEOUT, + }).click(); // click outside popover to close it cy.get('[data-cy=pageLoadDist]').click(); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_filters.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_filters.ts index 439003351aedb..2600e5d073328 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_filters.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_filters.ts @@ -5,6 +5,7 @@ */ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { DEFAULT_TIMEOUT } from './rum_dashboard'; When(/^the user filters by "([^"]*)"$/, (filterName) => { // wait for all loading to finish @@ -13,9 +14,13 @@ When(/^the user filters by "([^"]*)"$/, (filterName) => { cy.get(`#local-filter-${filterName}`).click(); if (filterName === 'os') { - cy.get('button.euiSelectableListItem[title="Mac OS X"]').click(); + cy.get('button.euiSelectableListItem[title="Mac OS X"]', { + timeout: DEFAULT_TIMEOUT, + }).click(); } else { - cy.get('button.euiSelectableListItem[title="DE"]').click(); + cy.get('button.euiSelectableListItem[title="DE"]', { + timeout: DEFAULT_TIMEOUT, + }).click(); } cy.get('[data-cy=applyFilter]').click(); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts new file mode 100644 index 0000000000000..9a3d7b52674b7 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { DEFAULT_TIMEOUT } from '../apm'; + +When('a user changes the selected service name', (filterName) => { + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get(`[data-cy=serviceNameFilter]`, { timeout: DEFAULT_TIMEOUT }).select( + 'opbean-client-rum' + ); +}); + +Then(`it displays relevant client metrics`, () => { + const clientMetrics = '[data-cy=client-metrics] .euiStat__title'; + + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get('.euiStat__title-isLoading').should('not.be.visible'); + + cy.get(clientMetrics).eq(2).invoke('text').snapshot(); + + cy.get(clientMetrics).eq(1).invoke('text').snapshot(); + + cy.get(clientMetrics).eq(0).invoke('text').snapshot(); +}); diff --git a/x-pack/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js index 483cc99df7470..6bab95635f558 100644 --- a/x-pack/plugins/apm/e2e/ingest-data/replay.js +++ b/x-pack/plugins/apm/e2e/ingest-data/replay.js @@ -69,6 +69,14 @@ function incrementSpinnerCount({ success }) { spinner.text = `Remaining: ${remaining}. Succeeded: ${requestProgress.succeeded}. Failed: ${requestProgress.failed}.`; } let iterIndex = 0; + +function setRumAgent(item) { + item.body = item.body.replace( + '"name":"client"', + '"name":"opbean-client-rum"' + ); +} + async function insertItem(item) { try { const url = `${APM_SERVER_URL}${item.url}`; @@ -78,6 +86,8 @@ async function insertItem(item) { if (item.url === '/intake/v2/rum/events') { if (iterIndex === userAgents.length) { + // set some event agent to opbean + setRumAgent(item); iterIndex = 0; } headers['User-Agent'] = userAgents[iterIndex]; 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 7ee8dfa496b57..4e1af6e0dc239 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 @@ -65,7 +65,7 @@ interface Props { function getCurrentTab( tabs: ErrorTab[] = [], currentTabKey: string | undefined -) { +): ErrorTab | {} { const selectedTab = tabs.find(({ key }) => key === currentTabKey); return selectedTab ? selectedTab : first(tabs) || {}; } @@ -78,7 +78,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) { } const tabs = getTabs(error); - const currentTab = getCurrentTab(tabs, urlParams.detailTab); + const currentTab = getCurrentTab(tabs, urlParams.detailTab) as ErrorTab; const errorUrl = error.error.page?.url || error.url?.full; 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 d71d5f2cb480d..3cd04ee032e56 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 @@ -10,7 +10,7 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import { scaleUtc } from 'd3-scale'; -import mean from 'lodash.mean'; +import { mean } from 'lodash'; import React from 'react'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 295f343b411a9..1625fb4c1409e 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -259,4 +259,13 @@ export const routes: BreadcrumbRoute[] = [ }), name: RouteName.RUM_OVERVIEW, }, + { + exact: true, + path: '/services/:serviceName/rum-overview', + component: () => , + breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', { + defaultMessage: 'Real User Monitoring', + }), + name: RouteName.RUM_OVERVIEW, + }, ]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index c9e475ef15316..3ddaa66b8de5e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -11,6 +11,7 @@ import { EuiSpacer, } from '@elastic/eui'; import React, { useMemo } from 'react'; +import { useRouteMatch } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; @@ -37,6 +38,10 @@ export function RumOverview() { urlParams: { start, end }, } = useUrlParams(); + const isRumServiceRoute = useRouteMatch( + '/services/:serviceName/rum-overview' + ); + const { data } = useFetcher( (callApmApi) => { if (start && end) { @@ -61,13 +66,17 @@ export function RumOverview() { - service.serviceName) ?? [] - } - /> - - + {!isRumServiceRoute && ( + <> + service.serviceName) ?? [] + } + /> + + {' '} + + )} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 81bdbdad805d6..ce60ffa4ba4e3 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -22,11 +22,17 @@ import { ServiceMap } from '../ServiceMap'; import { ServiceMetrics } from '../ServiceMetrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { TransactionOverview } from '../TransactionOverview'; -import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; import { RumOverview } from '../RumDashboard'; +import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; interface Props { - tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map'; + tab: + | 'transactions' + | 'errors' + | 'metrics' + | 'nodes' + | 'service-map' + | 'rum-overview'; } export function ServiceDetailTabs({ tab }: Props) { @@ -115,7 +121,7 @@ export function ServiceDetailTabs({ tab }: Props) { if (isRumAgentName(agentName)) { tabs.push({ link: ( - + {i18n.translate('xpack.apm.home.rumTabLabel', { defaultMessage: 'Real User Monitoring', })} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 8a3e2b1a02dac..26cff5e71b610 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -26,7 +26,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { padLeft, range } from 'lodash'; +import { padStart, range } from 'lodash'; import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; @@ -288,7 +288,7 @@ export class WatcherFlyout extends Component< // Generate UTC hours for Daily Report select field const intervalHours = range(24).map((i) => { - const hour = padLeft(i.toString(), 2, '0'); + const hour = padStart(i.toString(), 2, '0'); return { value: `${hour}:00`, text: `${hour}:00 UTC` }; }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index f0bc313ab4644..054476af28de1 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -110,7 +110,7 @@ function renderMustache( if (isObject(input)) { return Object.keys(input).reduce((acc, key) => { - const value = input[key]; + const value = (input as any)[key]; return { ...acc, [key]: renderMustache(value, ctx) }; }, {}); 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 c1bfce4cdca49..620ae6708eda0 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -12,7 +12,6 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import _ from 'lodash'; import React, { useMemo } from 'react'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx index abca9817bd69d..729ed9b10f827 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx @@ -11,21 +11,17 @@ */ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -const RumOverviewLink = (props: APMLinkExtendProps) => { - const { urlParams } = useUrlParams(); +interface RumOverviewLinkProps extends APMLinkExtendProps { + serviceName?: string; +} +export function RumOverviewLink({ + serviceName, + ...rest +}: RumOverviewLinkProps) { + const path = serviceName + ? `/services/${serviceName}/rum-overview` + : '/rum-overview'; - const persistedFilters = pickKeys( - urlParams, - 'transactionResult', - 'host', - 'containerId', - 'podName' - ); - - return ; -}; - -export { RumOverviewLink }; + return ; +} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx index e12a4a4831e17..0bb62bd8efcff 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -60,6 +60,7 @@ const ServiceNameFilter = ({ serviceNames }: Props) => { (props: Props) { } = useUrlParams(); const renderedItems = useMemo(() => { - // TODO: Use _.orderBy once we upgrade to lodash 4+ const sortedItems = sortItems - ? sortByOrder(items, sortField, sortDirection) + ? orderBy(items, sortField, sortDirection as 'asc' | 'desc') : items; return sortedItems.slice(page * pageSize, (page + 1) * pageSize); diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx index 01043f33ec7b7..b37146f3b3be5 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx @@ -87,7 +87,7 @@ export function getGroupedStackframes(stackframes: IStackframe[]) { !stackframe.exclude_from_grouping; // append to group - if (shouldAppend) { + if (prevGroup && shouldAppend) { prevGroup.stackframes.push(stackframe); return acc; } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts index 7f99939a0a0d0..d3a9ade3925a1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import { pick, isEmpty } from 'lodash'; +import { pickBy, isEmpty } from 'lodash'; import moment from 'moment'; import url from 'url'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; @@ -63,13 +63,13 @@ export const getSections = ({ const uptimeLink = url.format({ pathname: basePath.prepend('/app/uptime'), search: `?${fromQuery( - pick( + pickBy( { dateRangeStart: urlParams.rangeFrom, dateRangeEnd: urlParams.rangeTo, search: `url.domain:"${transaction.url?.domain}"`, }, - (val: string) => !isEmpty(val) + (val) => !isEmpty(val) ) )}`, }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 7aafa9e1fdcec..de60441f4faa0 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -6,7 +6,7 @@ import { EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import mean from 'lodash.mean'; +import { mean } from 'lodash'; import React, { useCallback } from 'react'; import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; diff --git a/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx index 9f72ac6d5916e..447e11eab5e41 100644 --- a/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx @@ -58,28 +58,29 @@ describe.skip('useFetcher', () => { expect(hook.result.current).toEqual(true); }); - it('is true for minimum 1000ms', () => { - hook = renderHook((isLoading) => useDelayedVisibility(isLoading), { - initialProps: false, - }); + // Disabled because it's flaky: https://github.com/elastic/kibana/issues/66389 + // it('is true for minimum 1000ms', () => { + // hook = renderHook((isLoading) => useDelayedVisibility(isLoading), { + // initialProps: false, + // }); - hook.rerender(true); + // hook.rerender(true); - act(() => { - jest.advanceTimersByTime(100); - }); + // act(() => { + // jest.advanceTimersByTime(100); + // }); - hook.rerender(false); - act(() => { - jest.advanceTimersByTime(900); - }); + // hook.rerender(false); + // act(() => { + // jest.advanceTimersByTime(900); + // }); - expect(hook.result.current).toEqual(true); + // expect(hook.result.current).toEqual(true); - act(() => { - jest.advanceTimersByTime(100); - }); + // act(() => { + // jest.advanceTimersByTime(100); + // }); - expect(hook.result.current).toEqual(false); - }); + // expect(hook.result.current).toEqual(false); + // }); }); diff --git a/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx index a26653d3d5294..99822c0bbc5ca 100644 --- a/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx +++ b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiPortal, EuiProgress } from '@elastic/eui'; -import { pick } from 'lodash'; +import { pickBy } from 'lodash'; import React, { Fragment, useMemo, useReducer } from 'react'; import { useDelayedVisibility } from '../components/shared/useDelayedVisibility'; @@ -26,7 +26,7 @@ function reducer(statuses: State, action: Action) { // Return an object with only the ids with `true` as their value, so that ids // that previously had `false` are removed and do not remain hanging around in // the object. - return pick( + return pickBy( { ...statuses, [action.id.toString()]: action.isLoading }, Boolean ); diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts index 9ce993e848488..9745c9ffdc705 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { compact, pick } from 'lodash'; +import { compact, pickBy } from 'lodash'; import datemath from '@elastic/datemath'; import { IUrlParams } from './types'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -61,8 +61,8 @@ export function getPathAsArray(pathname: string = '') { return compact(pathname.split('/')); } -export function removeUndefinedProps(obj: T): Partial { - return pick(obj, (value) => value !== undefined); +export function removeUndefinedProps(obj: T): Partial { + return pickBy(obj, (value) => value !== undefined); } export function getPathParams(pathname: string = ''): PathParams { @@ -104,6 +104,7 @@ export function getPathParams(pathname: string = ''): PathParams { serviceName, }; case 'service-map': + case 'rum-overview': return { serviceName, }; diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts index 589199221d7a9..79ccf8dbd6f9b 100644 --- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import mean from 'lodash.mean'; +import { mean } from 'lodash'; import { Theme } from '@kbn/ui-shared-deps/theme'; import { ApmFetchDataResponse, diff --git a/x-pack/plugins/apm/server/lib/service_map/__snapshots__/get_service_map_from_trace_ids.test.ts.snap b/x-pack/plugins/apm/server/lib/service_map/__snapshots__/get_service_map_from_trace_ids.test.ts.snap new file mode 100644 index 0000000000000..1f4a8a4367fad --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/__snapshots__/get_service_map_from_trace_ids.test.ts.snap @@ -0,0 +1,222 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getConnections transforms a list of paths into a list of connections filtered by service.name and environment 1`] = ` +Array [ + Object { + "destination": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + "source": Object { + "agent.name": "python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "172.18.0.6:3000", + "span.subtype": "http", + "span.type": "external", + }, + "source": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + }, + Object { + "destination": Object { + "agent.name": "python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "source": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + }, + Object { + "destination": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "source": Object { + "agent.name": "python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + "source": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "destination": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + "source": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "opbeans-python:3000", + "span.subtype": "http_rb", + "span.type": "ext", + }, + "source": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "destination": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + "source": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "opbeans-node:3000", + "span.subtype": "http", + "span.type": "external", + }, + "source": Object { + "agent.name": "python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "172.18.0.7:3000", + "span.subtype": "http", + "span.type": "external", + }, + "source": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + "source": Object { + "agent.name": "python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, + Object { + "destination": Object { + "agent.name": "python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + "source": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, + "source": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "93.184.216.34:80", + "span.subtype": "http", + "span.type": "external", + }, + "source": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "opbeans-ruby:3000", + "span.subtype": "http_rb", + "span.type": "ext", + }, + "source": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "redis", + "span.subtype": "redis", + "span.type": "cache", + }, + "source": Object { + "agent.name": "nodejs", + "service.environment": "production", + "service.name": "opbeans-node", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "opbeans-node:3000", + "span.subtype": "http_rb", + "span.type": "ext", + }, + "source": Object { + "agent.name": "ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + }, + }, + Object { + "destination": Object { + "span.destination.service.resource": "opbeans-ruby:3000", + "span.subtype": "http", + "span.type": "external", + }, + "source": Object { + "agent.name": "python", + "service.environment": "production", + "service.name": "opbeans-python", + }, + }, +] +`; diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts new file mode 100644 index 0000000000000..08c8aba5f0207 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + PROCESSOR_EVENT, + TRACE_ID, +} from '../../../common/elasticsearch_fieldnames'; +import { + ConnectionNode, + ExternalConnectionNode, + ServiceConnectionNode, +} from '../../../common/service_map'; +import { Setup } from '../helpers/setup_request'; + +export async function fetchServicePathsFromTraceIds( + setup: Setup, + traceIds: string[] +) { + const { indices, client } = setup; + + const serviceMapParams = { + index: [ + indices['apm_oss.spanIndices'], + indices['apm_oss.transactionIndices'], + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + [PROCESSOR_EVENT]: ['span', 'transaction'], + }, + }, + { + terms: { + [TRACE_ID]: traceIds, + }, + }, + ], + }, + }, + aggs: { + service_map: { + scripted_metric: { + init_script: { + lang: 'painless', + source: `state.eventsById = new HashMap(); + + String[] fieldsToCopy = new String[] { + 'parent.id', + 'service.name', + 'service.environment', + 'span.destination.service.resource', + 'trace.id', + 'processor.event', + 'span.type', + 'span.subtype', + 'agent.name' + }; + state.fieldsToCopy = fieldsToCopy;`, + }, + map_script: { + lang: 'painless', + source: `def id; + if (!doc['span.id'].empty) { + id = doc['span.id'].value; + } else { + id = doc['transaction.id'].value; + } + + def copy = new HashMap(); + copy.id = id; + + for(key in state.fieldsToCopy) { + if (!doc[key].empty) { + copy[key] = doc[key].value; + } + } + + state.eventsById[id] = copy`, + }, + combine_script: { + lang: 'painless', + source: `return state.eventsById;`, + }, + reduce_script: { + lang: 'painless', + source: ` + def getDestination ( def event ) { + def destination = new HashMap(); + destination['span.destination.service.resource'] = event['span.destination.service.resource']; + destination['span.type'] = event['span.type']; + destination['span.subtype'] = event['span.subtype']; + return destination; + } + + def processAndReturnEvent(def context, def eventId) { + if (context.processedEvents[eventId] != null) { + return context.processedEvents[eventId]; + } + + def event = context.eventsById[eventId]; + + if (event == null) { + return null; + } + + def service = new HashMap(); + service['service.name'] = event['service.name']; + service['service.environment'] = event['service.environment']; + service['agent.name'] = event['agent.name']; + + def basePath = new ArrayList(); + + def parentId = event['parent.id']; + def parent; + + if (parentId != null && parentId != event['id']) { + parent = processAndReturnEvent(context, parentId); + if (parent != null) { + /* copy the path from the parent */ + basePath.addAll(parent.path); + /* flag parent path for removal, as it has children */ + context.locationsToRemove.add(parent.path); + + /* if the parent has 'span.destination.service.resource' set, and the service is different, + we've discovered a service */ + + if (parent['span.destination.service.resource'] != null + && parent['span.destination.service.resource'] != "" + && (parent['service.name'] != event['service.name'] + || parent['service.environment'] != event['service.environment'] + ) + ) { + def parentDestination = getDestination(parent); + context.externalToServiceMap.put(parentDestination, service); + } + } + } + + def lastLocation = basePath.size() > 0 ? basePath[basePath.size() - 1] : null; + + def currentLocation = service; + + /* only add the current location to the path if it's different from the last one*/ + if (lastLocation == null || !lastLocation.equals(currentLocation)) { + basePath.add(currentLocation); + } + + /* if there is an outgoing span, create a new path */ + if (event['span.destination.service.resource'] != null + && event['span.destination.service.resource'] != '') { + def outgoingLocation = getDestination(event); + def outgoingPath = new ArrayList(basePath); + outgoingPath.add(outgoingLocation); + context.paths.add(outgoingPath); + } + + event.path = basePath; + + context.processedEvents[eventId] = event; + return event; + } + + def context = new HashMap(); + + context.processedEvents = new HashMap(); + context.eventsById = new HashMap(); + + context.paths = new HashSet(); + context.externalToServiceMap = new HashMap(); + context.locationsToRemove = new HashSet(); + + for (state in states) { + context.eventsById.putAll(state); + } + + for (entry in context.eventsById.entrySet()) { + processAndReturnEvent(context, entry.getKey()); + } + + def paths = new HashSet(); + + for(foundPath in context.paths) { + if (!context.locationsToRemove.contains(foundPath)) { + paths.add(foundPath); + } + } + + def response = new HashMap(); + response.paths = paths; + + def discoveredServices = new HashSet(); + + for(entry in context.externalToServiceMap.entrySet()) { + def map = new HashMap(); + map.from = entry.getKey(); + map.to = entry.getValue(); + discoveredServices.add(map); + } + response.discoveredServices = discoveredServices; + + return response;`, + }, + }, + }, + }, + }, + }; + + const serviceMapFromTraceIdsScriptResponse = await client.search( + serviceMapParams + ); + + return serviceMapFromTraceIdsScriptResponse as { + aggregations?: { + service_map: { + value: { + paths: ConnectionNode[][]; + discoveredServices: Array<{ + from: ExternalConnectionNode; + to: ServiceConnectionNode; + }>; + }; + }; + }; + }; +} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts new file mode 100644 index 0000000000000..a3a7e5c995bfe --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getConnections } from './get_service_map_from_trace_ids'; +import serviceMapFromTraceIdsScriptResponse from './mock_responses/get_service_map_from_trace_ids_script_response.json'; +import { PromiseReturnType } from '../../../typings/common'; +import { fetchServicePathsFromTraceIds } from './fetch_service_paths_from_trace_ids'; + +describe('getConnections', () => { + it('transforms a list of paths into a list of connections filtered by service.name and environment', () => { + const response = serviceMapFromTraceIdsScriptResponse as PromiseReturnType< + typeof fetchServicePathsFromTraceIds + >; + const serviceName = 'opbeans-node'; + const environment = 'production'; + + const connections = getConnections( + response.aggregations?.service_map.value.paths, + serviceName, + environment + ); + + expect(connections).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts index 01cbc1aa9b989..f6e331a09fa65 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts @@ -3,237 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { find, uniq } from 'lodash'; +import { find, uniqBy } from 'lodash'; import { - PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, - TRACE_ID, } from '../../../common/elasticsearch_fieldnames'; import { Connection, ConnectionNode, - ExternalConnectionNode, ServiceConnectionNode, } from '../../../common/service_map'; import { Setup } from '../helpers/setup_request'; - -export async function getServiceMapFromTraceIds({ - setup, - traceIds, - serviceName, - environment, -}: { - setup: Setup; - traceIds: string[]; - serviceName?: string; - environment?: string; -}) { - const { indices, client } = setup; - - const serviceMapParams = { - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], - body: { - size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: ['span', 'transaction'], - }, - }, - { - terms: { - [TRACE_ID]: traceIds, - }, - }, - ], - }, - }, - aggs: { - service_map: { - scripted_metric: { - init_script: { - lang: 'painless', - source: `state.eventsById = new HashMap(); - - String[] fieldsToCopy = new String[] { - 'parent.id', - 'service.name', - 'service.environment', - 'span.destination.service.resource', - 'trace.id', - 'processor.event', - 'span.type', - 'span.subtype', - 'agent.name' - }; - state.fieldsToCopy = fieldsToCopy;`, - }, - map_script: { - lang: 'painless', - source: `def id; - if (!doc['span.id'].empty) { - id = doc['span.id'].value; - } else { - id = doc['transaction.id'].value; - } - - def copy = new HashMap(); - copy.id = id; - - for(key in state.fieldsToCopy) { - if (!doc[key].empty) { - copy[key] = doc[key].value; - } - } - - state.eventsById[id] = copy`, - }, - combine_script: { - lang: 'painless', - source: `return state.eventsById;`, - }, - reduce_script: { - lang: 'painless', - source: ` - def getDestination ( def event ) { - def destination = new HashMap(); - destination['span.destination.service.resource'] = event['span.destination.service.resource']; - destination['span.type'] = event['span.type']; - destination['span.subtype'] = event['span.subtype']; - return destination; - } - - def processAndReturnEvent(def context, def eventId) { - if (context.processedEvents[eventId] != null) { - return context.processedEvents[eventId]; - } - - def event = context.eventsById[eventId]; - - if (event == null) { - return null; - } - - def service = new HashMap(); - service['service.name'] = event['service.name']; - service['service.environment'] = event['service.environment']; - service['agent.name'] = event['agent.name']; - - def basePath = new ArrayList(); - - def parentId = event['parent.id']; - def parent; - - if (parentId != null && parentId != event['id']) { - parent = processAndReturnEvent(context, parentId); - if (parent != null) { - /* copy the path from the parent */ - basePath.addAll(parent.path); - /* flag parent path for removal, as it has children */ - context.locationsToRemove.add(parent.path); - - /* if the parent has 'span.destination.service.resource' set, and the service is different, - we've discovered a service */ - - if (parent['span.destination.service.resource'] != null - && parent['span.destination.service.resource'] != "" - && (parent['service.name'] != event['service.name'] - || parent['service.environment'] != event['service.environment'] - ) - ) { - def parentDestination = getDestination(parent); - context.externalToServiceMap.put(parentDestination, service); - } - } - } - - def lastLocation = basePath.size() > 0 ? basePath[basePath.size() - 1] : null; - - def currentLocation = service; - - /* only add the current location to the path if it's different from the last one*/ - if (lastLocation == null || !lastLocation.equals(currentLocation)) { - basePath.add(currentLocation); - } - - /* if there is an outgoing span, create a new path */ - if (event['span.destination.service.resource'] != null - && event['span.destination.service.resource'] != '') { - def outgoingLocation = getDestination(event); - def outgoingPath = new ArrayList(basePath); - outgoingPath.add(outgoingLocation); - context.paths.add(outgoingPath); - } - - event.path = basePath; - - context.processedEvents[eventId] = event; - return event; - } - - def context = new HashMap(); - - context.processedEvents = new HashMap(); - context.eventsById = new HashMap(); - - context.paths = new HashSet(); - context.externalToServiceMap = new HashMap(); - context.locationsToRemove = new HashSet(); - - for (state in states) { - context.eventsById.putAll(state); - } - - for (entry in context.eventsById.entrySet()) { - processAndReturnEvent(context, entry.getKey()); - } - - def paths = new HashSet(); - - for(foundPath in context.paths) { - if (!context.locationsToRemove.contains(foundPath)) { - paths.add(foundPath); - } - } - - def response = new HashMap(); - response.paths = paths; - - def discoveredServices = new HashSet(); - - for(entry in context.externalToServiceMap.entrySet()) { - def map = new HashMap(); - map.from = entry.getKey(); - map.to = entry.getValue(); - discoveredServices.add(map); - } - response.discoveredServices = discoveredServices; - - return response;`, - }, - }, - }, - }, - }, - }; - - const serviceMapResponse = await client.search(serviceMapParams); - - const scriptResponse = serviceMapResponse.aggregations?.service_map.value as { - paths: ConnectionNode[][]; - discoveredServices: Array<{ - from: ExternalConnectionNode; - to: ServiceConnectionNode; - }>; - }; - - let paths = scriptResponse.paths; +import { fetchServicePathsFromTraceIds } from './fetch_service_paths_from_trace_ids'; + +export function getConnections( + paths?: ConnectionNode[][], + serviceName?: string, + environment?: string +) { + if (!paths) { + return []; + } if (serviceName || environment) { paths = paths.filter((path) => { @@ -257,26 +47,51 @@ export async function getServiceMapFromTraceIds({ }); } - const connections = uniq( - paths.flatMap((path) => { - return path.reduce((conns, location, index) => { - const prev = path[index - 1]; - if (prev) { - return conns.concat({ - source: prev, - destination: location, - }); - } - return conns; - }, [] as Connection[]); - }, [] as Connection[]), - (value, _index, array) => { - return find(array, value); - } + const connectionsArr = paths.flatMap((path) => { + return path.reduce((conns, location, index) => { + const prev = path[index - 1]; + if (prev) { + return conns.concat({ + source: prev, + destination: location, + }); + } + return conns; + }, [] as Connection[]); + }, [] as Connection[]); + + const connections = uniqBy(connectionsArr, (value) => + find(connectionsArr, value) ); + return connections; +} + +export async function getServiceMapFromTraceIds({ + setup, + traceIds, + serviceName, + environment, +}: { + setup: Setup; + traceIds: string[]; + serviceName?: string; + environment?: string; +}) { + const serviceMapFromTraceIdsScriptResponse = await fetchServicePathsFromTraceIds( + setup, + traceIds + ); + + const serviceMapScriptedAggValue = + serviceMapFromTraceIdsScriptResponse.aggregations?.service_map.value; + return { - connections, - discoveredServices: scriptResponse.discoveredServices, + connections: getConnections( + serviceMapScriptedAggValue?.paths, + serviceName, + environment + ), + discoveredServices: serviceMapScriptedAggValue?.discoveredServices ?? [], }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/mock_responses/get_service_map_from_trace_ids_script_response.json b/x-pack/plugins/apm/server/lib/service_map/mock_responses/get_service_map_from_trace_ids_script_response.json new file mode 100644 index 0000000000000..49d8efebbf43b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/mock_responses/get_service_map_from_trace_ids_script_response.json @@ -0,0 +1,1165 @@ +{ + "took": 43, + "timed_out": false, + "_shards": { "total": 6, "successful": 6, "skipped": 0, "failed": 0 }, + "hits": { + "total": { "value": 465, "relation": "eq" }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "service_map": { + "value": { + "paths": [ + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.6:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-python:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.6:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.7:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.7:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.7:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-python:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "93.184.216.34:80", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-python:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.7:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "cache" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-python:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "cache" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "cache" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-python:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.6:3000", + "span.type": "external" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "cache" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "ext" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "cache" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + }, + { + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "cache" + } + ], + [ + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + }, + { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + }, + { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "external" + } + ] + ], + "discoveredServices": [ + { + "from": { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "external" + }, + "to": { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + } + }, + { + "from": { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.7:3000", + "span.type": "external" + }, + "to": { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + } + }, + { + "from": { + "span.subtype": "http", + "span.destination.service.resource": "opbeans-ruby:3000", + "span.type": "external" + }, + "to": { + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby" + } + }, + { + "from": { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-node:3000", + "span.type": "ext" + }, + "to": { + "service.environment": "production", + "service.name": "opbeans-node", + "agent.name": "nodejs" + } + }, + { + "from": { + "span.subtype": "http_rb", + "span.destination.service.resource": "opbeans-python:3000", + "span.type": "ext" + }, + "to": { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + } + }, + { + "from": { + "span.subtype": "http", + "span.destination.service.resource": "172.18.0.6:3000", + "span.type": "external" + }, + "to": { + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python" + } + } + ] + } + } + } +} diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 835c00b8df239..2e394f44b25b1 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.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 { sortBy, pick, identity } from 'lodash'; +import { sortBy, pickBy, identity } from 'lodash'; import { ValuesType } from 'utility-types'; import { SERVICE_NAME, @@ -112,7 +112,7 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { id: matchedServiceNodes[0][SERVICE_NAME], }, ...matchedServiceNodes.map((serviceNode) => - pick(serviceNode, identity) + pickBy(serviceNode, identity) ) ), }; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 3f8d6b22cd000..0fc1f89a3723b 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -113,76 +113,244 @@ Object { `; exports[`services queries fetches the service items 1`] = ` -Object { - "body": Object { - "aggs": Object { - "services": Object { - "aggs": Object { - "agents": Object { - "terms": Object { - "field": "agent.name", - "size": 1, +Array [ + Object { + "body": Object { + "aggs": Object { + "services": Object { + "aggs": Object { + "average": Object { + "avg": Object { + "field": "transaction.duration.us", + }, }, }, - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", + "terms": Object { + "field": "service.name", + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", + "size": 0, + }, + Object { + "body": Object { + "aggs": Object { + "services": Object { + "aggs": Object { + "agent_name": Object { + "top_hits": Object { + "_source": Array [ + "agent.name", + ], + "size": 1, + }, }, }, - "environments": Object { - "terms": Object { - "field": "service.environment", + "terms": Object { + "field": "service.name", + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "metric", + "error", + "transaction", + ], + }, }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], + }, + Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 500, }, - "events": Object { - "terms": Object { - "field": "processor.event", + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", + }, + Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 500, }, }, - "terms": Object { - "field": "service.name", - "size": 500, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + ], }, }, + "size": 0, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], + "index": "myIndex", + }, + Object { + "body": Object { + "aggs": Object { + "services": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + }, }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, + "terms": Object { + "field": "service.name", + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, }, }, - }, - Object { - "term": Object { - "my.custom.ui.filter": "foo-bar", + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, }, - }, - ], + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + ], + }, }, + "size": 0, }, - "size": 0, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], -} +] `; exports[`services queries fetches the service transaction types 1`] = ` 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 acf052affabdb..14772e77fe1c2 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 @@ -3,14 +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 { mergeProjection } from '../../../../common/projections/util/merge_projection'; -import { - PROCESSOR_EVENT, - AGENT_NAME, - SERVICE_ENVIRONMENT, - TRANSACTION_DURATION, -} from '../../../../common/elasticsearch_fieldnames'; +import { joinByKey } from '../../../../common/utils/join_by_key'; import { PromiseReturnType } from '../../../../typings/common'; import { Setup, @@ -18,75 +11,45 @@ import { SetupUIFilters, } from '../../helpers/setup_request'; import { getServicesProjection } from '../../../../common/projections/services'; +import { + getTransactionDurationAverages, + getAgentNames, + getTransactionRates, + getErrorRates, + getEnvironments, +} from './get_services_items_stats'; export type ServiceListAPIResponse = PromiseReturnType; -export async function getServicesItems( - setup: Setup & SetupTimeRange & SetupUIFilters -) { - const { start, end, client } = setup; - - const projection = getServicesProjection({ setup }); - - const params = mergeProjection(projection, { - body: { - size: 0, - aggs: { - services: { - terms: { - ...projection.body.aggs.services.terms, - size: 500, - }, - aggs: { - avg: { - avg: { field: TRANSACTION_DURATION }, - }, - agents: { - terms: { field: AGENT_NAME, size: 1 }, - }, - events: { - terms: { field: PROCESSOR_EVENT }, - }, - environments: { - terms: { field: SERVICE_ENVIRONMENT }, - }, - }, - }, - }, - }, - }); - - const resp = await client.search(params); - const aggs = resp.aggregations; - - const serviceBuckets = aggs?.services.buckets || []; - - const items = serviceBuckets.map((bucket) => { - const eventTypes = bucket.events.buckets; - - const transactions = eventTypes.find((e) => e.key === 'transaction'); - const totalTransactions = transactions?.doc_count || 0; - - const errors = eventTypes.find((e) => e.key === 'error'); - const totalErrors = errors?.doc_count || 0; - - const deltaAsMinutes = (end - start) / 1000 / 60; - const transactionsPerMinute = totalTransactions / deltaAsMinutes; - const errorsPerMinute = totalErrors / deltaAsMinutes; - - const environmentsBuckets = bucket.environments.buckets; - const environments = environmentsBuckets.map( - (environmentBucket) => environmentBucket.key as string - ); - - return { - serviceName: bucket.key as string, - agentName: bucket.agents.buckets[0]?.key as string | undefined, - transactionsPerMinute, - errorsPerMinute, - avgResponseTime: bucket.avg.value, - environments, - }; - }); - - return items; +export type ServicesItemsSetup = Setup & SetupTimeRange & SetupUIFilters; +export type ServicesItemsProjection = ReturnType; + +export async function getServicesItems(setup: ServicesItemsSetup) { + const params = { + projection: getServicesProjection({ setup, noEvents: true }), + setup, + }; + + const [ + transactionDurationAverages, + agentNames, + transactionRates, + errorRates, + environments, + ] = await Promise.all([ + getTransactionDurationAverages(params), + getAgentNames(params), + getTransactionRates(params), + getErrorRates(params), + getEnvironments(params), + ]); + + const allMetrics = [ + ...transactionDurationAverages, + ...agentNames, + ...transactionRates, + ...errorRates, + ...environments, + ]; + + return joinByKey(allMetrics, 'serviceName'); } 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 new file mode 100644 index 0000000000000..c28bcad841ffd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -0,0 +1,309 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { arrayUnionToCallable } from '../../../../common/utils/array_union_to_callable'; +import { + PROCESSOR_EVENT, + TRANSACTION_DURATION, + AGENT_NAME, + SERVICE_ENVIRONMENT, +} from '../../../../common/elasticsearch_fieldnames'; +import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { + ServicesItemsSetup, + ServicesItemsProjection, +} from './get_services_items'; + +const MAX_NUMBER_OF_SERVICES = 500; + +const getDeltaAsMinutes = (setup: ServicesItemsSetup) => + (setup.end - setup.start) / 1000 / 60; + +interface AggregationParams { + setup: ServicesItemsSetup; + projection: ServicesItemsProjection; +} + +export const getTransactionDurationAverages = async ({ + setup, + projection, +}: AggregationParams) => { + const { client, indices } = setup; + + const response = await client.search( + mergeProjection(projection, { + size: 0, + index: indices['apm_oss.transactionIndices'], + body: { + query: { + bool: { + filter: projection.body.query.bool.filter.concat({ + term: { + [PROCESSOR_EVENT]: ProcessorEvent.transaction, + }, + }), + }, + }, + aggs: { + services: { + terms: { + ...projection.body.aggs.services.terms, + size: MAX_NUMBER_OF_SERVICES, + }, + aggs: { + average: { + avg: { + field: TRANSACTION_DURATION, + }, + }, + }, + }, + }, + }, + }) + ); + + const { aggregations } = response; + + if (!aggregations) { + return []; + } + + return aggregations.services.buckets.map((bucket) => ({ + serviceName: bucket.key as string, + avgResponseTime: bucket.average.value, + })); +}; + +export const getAgentNames = async ({ + setup, + projection, +}: AggregationParams) => { + const { client, indices } = setup; + const response = await client.search( + mergeProjection(projection, { + index: [ + indices['apm_oss.metricsIndices'], + indices['apm_oss.errorIndices'], + indices['apm_oss.transactionIndices'], + ], + body: { + size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { + terms: { + [PROCESSOR_EVENT]: [ + ProcessorEvent.metric, + ProcessorEvent.error, + ProcessorEvent.transaction, + ], + }, + }, + ], + }, + }, + aggs: { + services: { + terms: { + ...projection.body.aggs.services.terms, + size: MAX_NUMBER_OF_SERVICES, + }, + aggs: { + agent_name: { + top_hits: { + _source: [AGENT_NAME], + size: 1, + }, + }, + }, + }, + }, + }, + }) + ); + + const { aggregations } = response; + + if (!aggregations) { + return []; + } + + return aggregations.services.buckets.map((bucket) => ({ + serviceName: bucket.key as string, + agentName: (bucket.agent_name.hits.hits[0]?._source as { + agent: { + name: string; + }; + }).agent.name, + })); +}; + +export const getTransactionRates = async ({ + setup, + projection, +}: AggregationParams) => { + const { client, indices } = setup; + const response = await client.search( + mergeProjection(projection, { + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { + term: { + [PROCESSOR_EVENT]: ProcessorEvent.transaction, + }, + }, + ], + }, + }, + aggs: { + services: { + terms: { + ...projection.body.aggs.services.terms, + size: MAX_NUMBER_OF_SERVICES, + }, + }, + }, + }, + }) + ); + + const { aggregations } = response; + + if (!aggregations) { + return []; + } + + const deltaAsMinutes = getDeltaAsMinutes(setup); + + return arrayUnionToCallable(aggregations.services.buckets).map((bucket) => { + const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; + return { + serviceName: bucket.key as string, + transactionsPerMinute, + }; + }); +}; + +export const getErrorRates = async ({ + setup, + projection, +}: AggregationParams) => { + const { client, indices } = setup; + const response = await client.search( + mergeProjection(projection, { + index: indices['apm_oss.errorIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { + term: { + [PROCESSOR_EVENT]: ProcessorEvent.error, + }, + }, + ], + }, + }, + aggs: { + services: { + terms: { + ...projection.body.aggs.services.terms, + size: MAX_NUMBER_OF_SERVICES, + }, + }, + }, + }, + }) + ); + + const { aggregations } = response; + + if (!aggregations) { + return []; + } + + const deltaAsMinutes = getDeltaAsMinutes(setup); + + return aggregations.services.buckets.map((bucket) => { + const errorsPerMinute = bucket.doc_count / deltaAsMinutes; + return { + serviceName: bucket.key as string, + errorsPerMinute, + }; + }); +}; + +export const getEnvironments = async ({ + setup, + projection, +}: AggregationParams) => { + const { client, indices } = setup; + const response = await client.search( + mergeProjection(projection, { + index: [ + indices['apm_oss.metricsIndices'], + indices['apm_oss.errorIndices'], + indices['apm_oss.transactionIndices'], + ], + body: { + size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { + terms: { + [PROCESSOR_EVENT]: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + }, + ], + }, + }, + aggs: { + services: { + terms: { + ...projection.body.aggs.services.terms, + size: MAX_NUMBER_OF_SERVICES, + }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + }, + }, + }, + }, + }, + }, + }) + ); + + const { aggregations } = response; + + if (!aggregations) { + return []; + } + + return aggregations.services.buckets.map((bucket) => ({ + serviceName: bucket.key as string, + environments: bucket.environments.buckets.map((env) => env.key as string), + })); +}; diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index d90cd8bf13908..b2fe7efeaf959 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -40,7 +40,9 @@ describe('services queries', () => { it('fetches the service items', async () => { mock = await inspectSearchParams((setup) => getServicesItems(setup)); - expect(mock.params).toMatchSnapshot(); + const allParams = mock.spy.mock.calls.map((call) => call[0]); + + expect(allParams).toMatchSnapshot(); }); it('fetches the legacy data status', async () => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts index 81dba39e9d712..b04ff6764675d 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; import { ESResponse } from './fetcher'; function calculateRelativeImpacts(items: ITransactionGroup[]) { @@ -27,7 +27,7 @@ function calculateRelativeImpacts(items: ITransactionGroup[]) { const getBuckets = (response: ESResponse) => { if (response.aggregations) { - return sortByOrder( + return orderBy( response.aggregations.transaction_groups.buckets, ['sum.value'], ['desc'] diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 5af8b9f78cec1..3c48c14c2a471 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten, sortByOrder, last } from 'lodash'; +import { flatten, orderBy, last } from 'lodash'; import { SERVICE_NAME, SPAN_SUBTYPE, @@ -138,13 +138,13 @@ export async function getTransactionBreakdown({ }; const visibleKpis = resp.aggregations - ? sortByOrder(formatBucket(resp.aggregations), 'percentage', 'desc').slice( + ? orderBy(formatBucket(resp.aggregations), 'percentage', 'desc').slice( 0, MAX_KPIS ) : []; - const kpis = sortByOrder(visibleKpis, 'name').map((kpi, index) => { + const kpis = orderBy(visibleKpis, 'name').map((kpi, index) => { return { ...kpi, color: getVizColorForIndex(index), @@ -186,8 +186,8 @@ export async function getTransactionBreakdown({ // is drawn correctly. // If we set all values to 0, the chart always displays null values as 0, // and the chart looks weird. - const hasAnyValues = lastValues.some((value) => value.y !== null); - const hasNullValues = lastValues.some((value) => value.y === null); + const hasAnyValues = lastValues.some((value) => value?.y !== null); + const hasNullValues = lastValues.some((value) => value?.y === null); if (hasAnyValues && hasNullValues) { Object.values(updatedSeries).forEach((series) => { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 31bc0563ec13f..588d5c7896db9 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.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 { cloneDeep, sortByOrder } from 'lodash'; +import { cloneDeep, orderBy } from 'lodash'; import { UIFilters } from '../../../../typings/ui_filters'; import { Projection } from '../../../../common/projections/typings'; import { PromiseReturnType } from '../../../../../observability/typings/common'; @@ -47,7 +47,7 @@ export async function getLocalUIFilters({ return { ...filter, - options: sortByOrder( + options: orderBy( buckets.map((bucket) => { return { name: bucket.key as string, 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 b21f0ea8d32db..92f52dd1552d6 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -140,6 +140,7 @@ export function createApi() { // Only return values for parameters that have runtime types, // but always include query as _debug is always set even if // it's not defined in the route. + // @ts-ignore params: pick(parsedParams, ...Object.keys(params), 'query'), config, logger, diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 08eba00251e26..74ab717b8de59 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; import Boom from 'boom'; -import { unique } from 'lodash'; +import { uniq } from 'lodash'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServices } from '../lib/services/get_services'; @@ -160,7 +160,7 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ ...body.service, name: path.serviceName, }, - tags: unique(['apm'].concat(body.tags ?? [])), + tags: uniq(['apm'].concat(body.tags ?? [])), }); }, })); diff --git a/x-pack/plugins/beats_management/public/components/enroll_beats.tsx b/x-pack/plugins/beats_management/public/components/enroll_beats.tsx index e609cd83587ce..5bf0f51f48355 100644 --- a/x-pack/plugins/beats_management/public/components/enroll_beats.tsx +++ b/x-pack/plugins/beats_management/public/components/enroll_beats.tsx @@ -18,7 +18,7 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import React from 'react'; import { CMBeat } from '../../../../legacy/plugins/beats_management/common/domain_types'; @@ -93,7 +93,7 @@ export class EnrollBeat extends React.Component } const cmdText = `${this.state.command .replace('{{beatType}}', this.state.beatType) - .replace('{{beatTypeInCaps}}', capitalize(this.state.beatType))} enroll ${ + .replace('{{beatTypeInCaps}}', upperFirst(this.state.beatType))} enroll ${ window.location.protocol }//${window.location.host}${this.props.frameworkBasePath} ${this.props.enrollmentToken}`; @@ -183,7 +183,7 @@ export class EnrollBeat extends React.Component id="xpack.beatsManagement.enrollBeat.yourBeatTypeHostTitle" defaultMessage="On the host where your {beatType} is installed, run:" values={{ - beatType: capitalize(this.state.beatType), + beatType: upperFirst(this.state.beatType), }} /> @@ -220,7 +220,7 @@ export class EnrollBeat extends React.Component id="xpack.beatsManagement.enrollBeat.waitingBeatTypeToEnrollTitle" defaultMessage="Waiting for {beatType} to enroll…" values={{ - beatType: capitalize(this.state.beatType), + beatType: upperFirst(this.state.beatType), }} /> diff --git a/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx b/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx index 947e22ee29089..ebac34afa016b 100644 --- a/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx +++ b/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import _ from 'lodash'; import React from 'react'; import { EuiLink } from '@elastic/eui'; diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx index 94e4ca46aec19..6bbf269711fbd 100644 --- a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx +++ b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiToolTip, IconColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { sortBy, uniq } from 'lodash'; +import { sortBy, uniqBy } from 'lodash'; import moment from 'moment'; import React from 'react'; import { @@ -226,7 +226,7 @@ export const BeatsTableType: TableType = { // render: (tags?: BeatTag[]) => // tags && tags.length ? ( // - // {moment(first(sortByOrder(tags, ['last_updated'], ['desc'])).last_updated).fromNow()} + // {moment(first(orderBy(tags, ['last_updated'], ['desc'])).last_updated).fromNow()} // // ) : null, // sortable: true, @@ -249,7 +249,7 @@ export const BeatsTableType: TableType = { name: i18n.translate('xpack.beatsManagement.beatsTable.typeLabel', { defaultMessage: 'Type', }), - options: uniq( + options: uniqBy( data.map(({ type }: { type: any }) => ({ value: type })), 'value' ), diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts index 8e3f58b18f391..24a7e5c3af8fa 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -32,14 +32,14 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { } public async getAll() { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])) as CMBeat[]; } public async getBeatsWithTag(tagId: string): Promise { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])) as CMBeat[]; } public async getBeatWithToken(enrollmentToken: string): Promise { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token']))[0]; + return this.beatsDB.map((beat: any) => omit(beat, ['access_token']))[0] as CMBeat | null; } public async removeTagsFromBeats( removals: BeatsTagAssignment[] @@ -66,11 +66,11 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return beat; }); - return response.map((item: CMBeat, resultIdx: number) => ({ + return response.map((item: CMBeat, resultIdx: number) => ({ idxInRequest: removals[resultIdx].idxInRequest, result: 'updated', status: 200, - })); + })) as any; } public async assignTagsToBeats( diff --git a/x-pack/plugins/beats_management/public/lib/framework.ts b/x-pack/plugins/beats_management/public/lib/framework.ts index 9e4271c683415..63a81e0895348 100644 --- a/x-pack/plugins/beats_management/public/lib/framework.ts +++ b/x-pack/plugins/beats_management/public/lib/framework.ts @@ -58,6 +58,6 @@ export class FrameworkLib { public currentUserHasOneOfRoles(roles: string[]) { // If the user has at least one of the roles requested, the returnd difference will be less // then the orig array size. difference only compares based on the left side arg - return difference(roles, get(this.currentUser, 'roles', [])).length < roles.length; + return difference(roles, get(this.currentUser, 'roles', []) as string[]).length < roles.length; } } diff --git a/x-pack/plugins/canvas/.storybook/webpack.config.js b/x-pack/plugins/canvas/.storybook/webpack.config.js index 45a5303d8b0db..3148a6742f76a 100644 --- a/x-pack/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/plugins/canvas/.storybook/webpack.config.js @@ -80,7 +80,7 @@ module.exports = async ({ config }) => { prependData(loaderContext) { return `@import ${stringifyRequest( loaderContext, - path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') )};\n`; }, sassOptions: { @@ -199,7 +199,6 @@ module.exports = async ({ config }) => { config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve(__dirname, '../tasks/mocks/uiAbsoluteToParsedUrl'); config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); - config.resolve.alias['src/legacy/ui/public/styles/styling_constants'] = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss'); config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); return config; diff --git a/x-pack/plugins/canvas/.storybook/webpack.dll.config.js b/x-pack/plugins/canvas/.storybook/webpack.dll.config.js index 0a648e861b386..5fdc4519f3bd7 100644 --- a/x-pack/plugins/canvas/.storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/.storybook/webpack.dll.config.js @@ -39,7 +39,6 @@ module.exports = { 'highlight.js', 'html-entities', 'jquery', - 'lodash.clone', 'lodash', 'markdown-it', 'mocha', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts index b568f18924869..c32c553fffc1b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, map, groupBy } from 'lodash'; -// @ts-expect-error lodash.keyby imports invalid member from @types/lodash -import keyBy from 'lodash.keyby'; +import { get, keyBy, map, groupBy } from 'lodash'; // @ts-expect-error untyped local import { getColorsFromPalette } from '../../../common/lib/get_colors_from_palette'; // @ts-expect-error untyped local diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_tick_hash.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_tick_hash.ts index 4839db047c871..21166454e478f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_tick_hash.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/get_tick_hash.ts @@ -20,11 +20,13 @@ export const getTickHash = (columns: PointSeriesColumns, rows: DatatableRow[]) = }; if (get(columns, 'x.type') === 'string') { - sortBy(rows, ['x']).forEach((row) => { - if (!ticks.x.hash[row.x]) { - ticks.x.hash[row.x] = ticks.x.counter++; - } - }); + sortBy(rows, ['x']) + .reverse() + .forEach((row) => { + if (!ticks.x.hash[row.x]) { + ticks.x.hash[row.x] = ticks.x.counter++; + } + }); } if (get(columns, 'y.type') === 'string') { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts index 0b4583f4581ae..4ffd2ff3e0c96 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-expect-error no @typed def -import keyBy from 'lodash.keyby'; -import { groupBy, get, set, map, sortBy } from 'lodash'; +import { groupBy, get, keyBy, set, map, sortBy } from 'lodash'; import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions'; // @ts-expect-error untyped local import { getColorsFromPalette } from '../../../../common/lib/get_colors_from_palette'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/series_style_to_flot.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/series_style_to_flot.ts index 6fbaee8736a50..e4b710240de19 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/series_style_to_flot.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/series_style_to_flot.ts @@ -12,12 +12,12 @@ export const seriesStyleToFlot = (seriesStyle: SeriesStyle) => { return {}; } - const lines = get(seriesStyle, 'lines'); - const bars = get(seriesStyle, 'bars'); - const fill = get(seriesStyle, 'fill'); - const color = get(seriesStyle, 'color'); - const stack = get(seriesStyle, 'stack'); - const horizontal = get(seriesStyle, 'horizontalBars', false); + const lines = get(seriesStyle, 'lines'); + const bars = get(seriesStyle, 'bars'); + const fill = get(seriesStyle, 'fill'); + const color = get(seriesStyle, 'color'); + const stack = get(seriesStyle, 'stack'); + const horizontal = get(seriesStyle, 'horizontalBars', false); const flotStyle = { numbers: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts index bae80d3c33510..f79f189f363d4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-expect-error untyped library -import uniqBy from 'lodash.uniqby'; -// @ts-expect-error untyped Elastic library +// @ts-expect-error Untyped Elastic library import { evaluate } from 'tinymath'; -import { groupBy, zipObject, omit } from 'lodash'; +import { groupBy, zipObject, omit, uniqBy } from 'lodash'; import moment from 'moment'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter.tsx index 8d28287b32066..487f17fb89d12 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter.tsx @@ -34,9 +34,9 @@ export interface FilterMeta { function getFilterMeta(filter: string): FilterMeta { const ast = fromExpression(filter); - const column = get(ast, 'chain[0].arguments.column[0]'); - const start = get(ast, 'chain[0].arguments.from[0]'); - const end = get(ast, 'chain[0].arguments.to[0]'); + const column = get(ast, 'chain[0].arguments.column[0]') as string; + const start = get(ast, 'chain[0].arguments.from[0]') as string; + const end = get(ast, 'chain[0].arguments.to[0]') as string; return { column, start, end }; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx index a33d000a1f656..8ae61f7197ee8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx @@ -40,7 +40,7 @@ export const PaletteArgInput: FC = ({ onValueChange, argId, argValue, ren return astObj; }) as string[]; - const gradient = get(chain[0].arguments.gradient, '[0]'); + const gradient = get(chain[0].arguments.gradient, '[0]') as boolean; const palette = identifyPalette({ colors, gradient }); if (palette) { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/plot.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/plot.js index 1449bddf322b5..05ecf467a1d35 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/plot.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/plot.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { map, uniq } from 'lodash'; +import { map, uniqBy } from 'lodash'; import { getState, getValue } from '../../../public/lib/resolved_arg'; import { legendOptions } from '../../../public/lib/legend_options'; import { ViewStrings } from '../../../i18n'; @@ -72,6 +72,6 @@ export const plot = () => ({ if (getState(context) !== 'ready') { return { labels: [] }; } - return { labels: uniq(map(getValue(context).rows, 'color').filter((v) => v !== undefined)) }; + return { labels: uniqBy(map(getValue(context).rows, 'color').filter((v) => v !== undefined)) }; }, }); diff --git a/x-pack/plugins/canvas/common/lib/pivot_object_array.ts b/x-pack/plugins/canvas/common/lib/pivot_object_array.ts index c098b7772ef11..2bc52fb0eaafc 100644 --- a/x-pack/plugins/canvas/common/lib/pivot_object_array.ts +++ b/x-pack/plugins/canvas/common/lib/pivot_object_array.ts @@ -11,10 +11,7 @@ const isString = (val: any): boolean => typeof val === 'string'; export function pivotObjectArray< RowType extends { [key: string]: any }, ReturnColumns extends string | number | symbol = keyof RowType ->( - rows: RowType[], - columns?: string[] -): { [Column in ReturnColumns]: Column extends keyof RowType ? Array : never } { +>(rows: RowType[], columns?: string[]): Record { const columnNames = columns || Object.keys(rows[0]); if (!columnNames.every(isString)) { throw new Error('Columns should be an array of strings'); diff --git a/x-pack/plugins/canvas/public/components/enhance/error_boundary.tsx b/x-pack/plugins/canvas/public/components/enhance/error_boundary.tsx index 134efe61c9dcb..c0ed14965cbd3 100644 --- a/x-pack/plugins/canvas/public/components/enhance/error_boundary.tsx +++ b/x-pack/plugins/canvas/public/components/enhance/error_boundary.tsx @@ -4,38 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FunctionComponent, ReactChildren } from 'react'; +import React, { ErrorInfo, FC, ReactElement } from 'react'; import { withState, withHandlers, lifecycle, mapProps, compose } from 'recompose'; import PropTypes from 'prop-types'; import { omit } from 'lodash'; -type ResetErrorState = ({ - setError, - setErrorInfo, -}: { - setError: Function; - setErrorInfo: Function; -}) => void; - interface Props { - error: Error; - errorInfo: any; - resetErrorState: ResetErrorState; + error?: Error; + errorInfo?: ErrorInfo; + resetErrorState: (state: { error: Error; errorInfo: ErrorInfo }) => void; + setError: (error: Error | null) => void; + setErrorInfo: (info: ErrorInfo | null) => void; + children: (props: ChildrenProps) => ReactElement | null; } -interface ComponentProps extends Props { - children: (props: Props) => ReactChildren; -} +type ComponentProps = Pick; +type ChildrenProps = Omit; -const ErrorBoundaryComponent: FunctionComponent = (props) => ( - - {props.children({ - error: props.error, - errorInfo: props.errorInfo, - resetErrorState: props.resetErrorState, - })} - -); +const ErrorBoundaryComponent: FC = (props) => { + const { children, ...rest } = props; + return <>{children(rest)}; +}; ErrorBoundaryComponent.propTypes = { children: PropTypes.func.isRequired, @@ -44,33 +33,22 @@ ErrorBoundaryComponent.propTypes = { resetErrorState: PropTypes.func.isRequired, }; -interface HOCProps { - setError: Function; - setErrorInfo: Function; -} - -interface HandlerProps { - resetErrorState: ResetErrorState; -} - -export const errorBoundaryHoc = compose( +export const errorBoundaryHoc = compose>( withState('error', 'setError', null), withState('errorInfo', 'setErrorInfo', null), - withHandlers({ + withHandlers, Pick>({ resetErrorState: ({ setError, setErrorInfo }) => () => { setError(null); setErrorInfo(null); }, }), - lifecycle({ + lifecycle({ componentDidCatch(error, errorInfo) { this.props.setError(error); this.props.setErrorInfo(errorInfo); }, }), - mapProps>((props) => - omit(props, ['setError', 'setErrorInfo']) - ) + mapProps((props) => omit(props, ['setError', 'setErrorInfo'])) ); export const ErrorBoundary = errorBoundaryHoc(ErrorBoundaryComponent); diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form.js b/x-pack/plugins/canvas/public/components/function_form/function_form.js index 8c9f8847d8eed..062f782942a82 100644 --- a/x-pack/plugins/canvas/public/components/function_form/function_form.js +++ b/x-pack/plugins/canvas/public/components/function_form/function_form.js @@ -32,7 +32,6 @@ const branches = [ export const FunctionForm = compose(...branches)(FunctionFormComponent); FunctionForm.propTypes = { - expressionType: PropTypes.object, context: PropTypes.object, expressionType: PropTypes.object, }; diff --git a/x-pack/plugins/canvas/public/components/item_grid/item_grid.tsx b/x-pack/plugins/canvas/public/components/item_grid/item_grid.tsx index 234f505071669..b9c879a27fd9c 100644 --- a/x-pack/plugins/canvas/public/components/item_grid/item_grid.tsx +++ b/x-pack/plugins/canvas/public/components/item_grid/item_grid.tsx @@ -19,13 +19,13 @@ export interface Props { */ itemsPerRow?: number; /** A function with which to iterate upon the items collection, producing nodes. */ - children: (item: T) => ReactElement; + children: (item: T) => ReactElement; } // We need this type in order to define propTypes on the object. It's a bit redundant, // but TS needs to know that ItemGrid can have propTypes defined on it. interface ItemGridType { - (props: Props): ReactElement; + (props: Props): ReactElement; propTypes?: ValidationMap>; } @@ -35,16 +35,22 @@ export const ItemGrid: ItemGridType = function ItemGridFunc({ children, }: Props) { const reducedRows = items.reduce( - (rows: Array>>, item: any) => { - if (last(rows).length >= itemsPerRow) { + (rows: ReactElement[][], item: T) => { + let end = last(rows); + + if (end && end.length >= itemsPerRow) { rows.push([]); } - last(rows).push(children(item)); + end = last(rows); + + if (end) { + end.push(children(item)); + } return rows; }, - [[]] as Array>> + [[]] as ReactElement[][] ); return ( diff --git a/x-pack/plugins/canvas/public/components/workpad_config/index.ts b/x-pack/plugins/canvas/public/components/workpad_config/index.ts index e417821fd4f67..c69a1fd9b8137 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_config/index.ts @@ -17,12 +17,12 @@ const mapStateToProps = (state: State) => { const workpad = getWorkpad(state); return { - name: get(workpad, 'name'), + name: get(workpad, 'name'), size: { - width: get(workpad, 'width'), - height: get(workpad, 'height'), + width: get(workpad, 'width'), + height: get(workpad, 'height'), }, - css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), + css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), }; }; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 28cfac11e76bd..af4e3af6db693 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -19,7 +19,7 @@ import { EuiFilePicker, EuiLink, } from '@elastic/eui'; -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { Paginate } from '../paginate'; @@ -369,7 +369,7 @@ export class WorkpadLoader extends React.PureComponent { if (!createPending && !isLoading) { const { workpads } = this.props.workpads; - sortedWorkpads = sortByOrder(workpads, [sortField, '@timestamp'], [sortDirection, 'desc']); + sortedWorkpads = orderBy(workpads, [sortField, '@timestamp'], [sortDirection, 'desc']); } return ( diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx index 065b84c490d20..43d5d5f939ce0 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx @@ -18,7 +18,7 @@ import { Direction, SortDirection, } from '@elastic/eui'; -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; // @ts-ignore untyped local import { EuiBasicTableColumn } from '@elastic/eui'; import { Paginate, PaginateChildProps } from '../paginate'; @@ -182,7 +182,7 @@ export class WorkpadTemplates extends React.PureComponent< render() { const { templates } = this.props; const { sortField, sortDirection, searchTerm, filterTags } = this.state; - const sortedTemplates = sortByOrder(templates, [sortField, 'name'], [sortDirection, 'asc']); + const sortedTemplates = orderBy(templates, [sortField, 'name'], [sortDirection, 'asc']); const filteredTemplates = sortedTemplates.filter(({ name = '', help = '', tags = [] }) => { const tagMatch = filterTags.length diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx index e0fe6e60c1dab..f02407ba2897d 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx @@ -46,7 +46,7 @@ export const ExtendedTemplate: FunctionComponent = (props) => { name = typeInstance.name; } - const fields = get(typeInstance, 'options.include', []); + const fields: string[] = get(typeInstance, 'options.include', []); const hasPropFields = fields.some((field) => ['lines', 'bars', 'points'].indexOf(field) !== -1); const handleChange: (key: T, val: ChangeEvent) => void = ( diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index 48f4a41c7690a..ecde5d2eb255b 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -36,7 +36,7 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = return allFilters.filter((filter: string) => { const ast = fromExpression(filter); - const expGroups = get(ast, 'chain[0].arguments.filterGroup', []); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); return expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup)); }); } diff --git a/x-pack/plugins/canvas/public/lib/keymap.ts b/x-pack/plugins/canvas/public/lib/keymap.ts index 7ca93f440087e..f713da5419b3e 100644 --- a/x-pack/plugins/canvas/public/lib/keymap.ts +++ b/x-pack/plugins/canvas/public/lib/keymap.ts @@ -153,10 +153,12 @@ export const keymap: KeyMap = { displayName: namespaceDisplayNames.PRESENTATION, FULLSCREEN: fullscreenShortcut, FULLSCREEN_EXIT: getShortcuts('esc', { help: shortcutHelp.FULLSCREEN_EXIT }), + // @ts-expect-error TODO: figure out why lodash is inferring booleans, rather than ShortcutMap. PREV: mapValues(previousPageShortcut, (osShortcuts: string[], key?: string) => // adds 'backspace' and 'left' to list of shortcuts per OS key === 'help' ? osShortcuts : osShortcuts.concat(['backspace', 'left']) ), + // @ts-expect-error TODO: figure out why lodash is inferring booleans, rather than ShortcutMap. NEXT: mapValues(nextPageShortcut, (osShortcuts: string[], key?: string) => // adds 'space' and 'right' to list of shortcuts per OS key === 'help' ? osShortcuts : osShortcuts.concat(['space', 'right']) diff --git a/x-pack/plugins/canvas/public/lib/modify_path.js b/x-pack/plugins/canvas/public/lib/modify_path.js index b4b2354b4cae0..714a616679bc9 100644 --- a/x-pack/plugins/canvas/public/lib/modify_path.js +++ b/x-pack/plugins/canvas/public/lib/modify_path.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import toPath from 'lodash.topath'; +import { toPath } from 'lodash'; export function prepend(path, value) { return toPath(value).concat(toPath(path)); diff --git a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx index 17e2712c44b8d..f4e715b1bbc49 100644 --- a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx +++ b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ComponentType, FunctionComponent } from 'react'; +import React, { ComponentType, FC } from 'react'; import { unmountComponentAtNode, render } from 'react-dom'; import PropTypes from 'prop-types'; import { I18nProvider } from '@kbn/i18n/react'; @@ -16,9 +16,9 @@ interface Props { } export const templateFromReactComponent = (Component: ComponentType) => { - const WrappedComponent: FunctionComponent = (props) => ( + const WrappedComponent: FC = (props) => ( - {({ error }: { error: Error }) => { + {({ error }) => { if (error) { props.renderError(); return null; diff --git a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts index 766e27d95da9b..770d4403f8587 100644 --- a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts +++ b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts @@ -12,7 +12,7 @@ import { prepend } from '../../lib/modify_path'; import { State } from '../../../types'; export function getArgs(state: State) { - return get(state, ['transient', 'resolvedArgs']); + return get(state, ['transient', 'resolvedArgs']); } export function getArg(state: State, path: any[]) { diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index 0f4953ff56d98..83f4984b4a300 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -50,7 +50,10 @@ export function getWorkpadPersisted(state: State) { } export function getWorkpadInfo(state: State): WorkpadInfo { - return omit(getWorkpad(state), ['pages']); + return { + ...getWorkpad(state), + pages: undefined, + }; } export function isWriteable(state: State): boolean { @@ -308,7 +311,7 @@ export function getElements( } const page = getPageById(state, id); - const elements = get(page, 'elements'); + const elements = get(page, 'elements'); if (!elements) { return []; @@ -318,6 +321,8 @@ export function getElements( // due to https://github.com/elastic/kibana-canvas/issues/260 // TODO: remove this once it's been in the wild a bit if (!withAst) { + // @ts-expect-error 'ast' is no longer on the CanvasElement type, but since we + // have JS calling into this, we can't be certain this call isn't necessary. return elements.map((el) => omit(el, ['ast'])); } @@ -330,11 +335,13 @@ const augment = (type: string) => (n: T): ...(type === 'group' && { expression: 'shape fill="rgba(255,255,255,0)" | render' }), // fixme unify with mw/aeroelastic }); -const getNodesOfPage = (page: CanvasPage): CanvasElement[] => { - const elements = get(page, 'elements').map(augment('element')); - const groups = get(page, 'groups', []).map(augment('group')); +const getNodesOfPage = (page: CanvasPage): Array => { + const elements: Array = get(page, 'elements').map( + augment('element') + ); + const groups = get(page, 'groups', [] as CanvasGroup[]).map(augment('group')); - return elements.concat(groups as CanvasElement[]); + return elements.concat(groups); }; export function getNodesForPage(page: CanvasPage, withAst: true): PositionedElement[]; @@ -343,7 +350,11 @@ export function getNodesForPage( page: CanvasPage, withAst: boolean ): CanvasElement[] | PositionedElement[]; -export function getNodesForPage(page: CanvasPage, withAst: boolean): CanvasElement[] { + +export function getNodesForPage( + page: CanvasPage, + withAst: boolean +): Array { const elements = getNodesOfPage(page); if (!elements) { @@ -354,9 +365,12 @@ export function getNodesForPage(page: CanvasPage, withAst: boolean): CanvasEleme // due to https://github.com/elastic/kibana-canvas/issues/260 // TODO: remove this once it's been in the wild a bit if (!withAst) { + // @ts-expect-error 'ast' is no longer on the CanvasElement type, but since we + // have JS calling into this, we can't be certain this call isn't necessary. return elements.map((el) => omit(el, ['ast'])); } + // @ts-expect-error All of this AST business needs to be cleaned up. return elements.map(appendAst); } @@ -407,7 +421,7 @@ export function getResolvedArgs(state: State, elementId: string, path: any): any if (!elementId) { return; } - const args = get(state, ['transient', 'resolvedArgs', elementId]); + const args = get(state, ['transient', 'resolvedArgs', elementId]) as any; if (path) { return get(args, path); } diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 78a34a58f5f78..9cd2bdabd3f45 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Canvas core @import 'hackery'; @import 'main'; diff --git a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts index 3ada8e7b4efdc..7b39e8b83b045 100644 --- a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts @@ -113,7 +113,7 @@ const customElementCollector: TelemetryCollector = async function customElementC const esResponse = await callCluster('search', customElementParams); - if (get(esResponse, 'hits.hits.length') > 0) { + if (get(esResponse, 'hits.hits.length') > 0) { const customElements = esResponse.hits.hits.map((hit) => hit._source[CUSTOM_ELEMENT_TYPE]); return summarizeCustomElements(customElements); } diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts index 420b785771bfe..9f71edcc05bf2 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import clonedeep from 'lodash.clonedeep'; +import { cloneDeep } from 'lodash'; import { summarizeWorkpads } from './workpad_collector'; import { workpads } from '../../__tests__/fixtures/workpads'; @@ -53,7 +53,7 @@ describe('usage collector handle es response data', () => { }); it('should collect correctly if an expression has null as an argument (possible sub-expression)', () => { - const workpad = clonedeep(workpads[0]); + const workpad = cloneDeep(workpads[0]); workpad.pages[0].elements[0].expression = 'toast butter=null'; const mockWorkpads = [workpad]; @@ -67,7 +67,7 @@ describe('usage collector handle es response data', () => { }); it('should fail gracefully if workpad has 0 pages (corrupted workpad)', () => { - const workpad = clonedeep(workpads[0]); + const workpad = cloneDeep(workpads[0]); workpad.pages = []; const mockWorkpadsCorrupted = [workpad]; const usage = summarizeWorkpads(mockWorkpadsCorrupted); diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts index 3d394afaeba50..4b00d061c17ce 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts @@ -133,8 +133,8 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr total: elementsTotal, per_page: { avg: elementsTotal / elementCounts.length, - min: arrayMin(elementCounts), - max: arrayMax(elementCounts), + min: arrayMin(elementCounts) || 0, + max: arrayMax(elementCounts) || 0, }, } : undefined; @@ -145,8 +145,8 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr in_use: Array.from(functionSet), per_element: { avg: functionsTotal / functionCounts.length, - min: arrayMin(functionCounts), - max: arrayMax(functionCounts), + min: arrayMin(functionCounts) || 0, + max: arrayMax(functionCounts) || 0, }, } : undefined; @@ -170,7 +170,7 @@ const workpadCollector: TelemetryCollector = async function (kibanaIndex, callCl const esResponse = await callCluster('search', searchParams); - if (get(esResponse, 'hits.hits.length') > 0) { + if (get(esResponse, 'hits.hits.length') > 0) { const workpads = esResponse.hits.hits.map((hit) => hit._source[CANVAS_TYPE]); return summarizeWorkpads(workpads); } diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts index 021ac41d88d19..9dae2047c30bf 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -7,6 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { omit } from 'lodash'; import { KibanaResponseFactory, SavedObjectsClientContract } from 'src/core/server'; +import { CanvasWorkpad } from '../../../types'; import { RouteInitializerDeps } from '../'; import { CANVAS_TYPE, @@ -14,7 +15,6 @@ import { API_ROUTE_WORKPAD_STRUCTURES, API_ROUTE_WORKPAD_ASSETS, } from '../../../common/lib/constants'; -import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; @@ -33,8 +33,8 @@ const workpadUpdateHandler = async ( ) => { const now = new Date().toISOString(); - const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); - await savedObjectsClient.create( + const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); + await savedObjectsClient.create( CANVAS_TYPE, { ...workpadObject.attributes, diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 66b0a7bc558cb..1a5a21985ba72 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -188,7 +188,7 @@ module.exports = { prependData(loaderContext) { return `@import ${stringifyRequest( loaderContext, - path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') )};\n`; }, webpackImporter: false, diff --git a/x-pack/plugins/dashboard_mode/public/plugin.ts b/x-pack/plugins/dashboard_mode/public/plugin.ts index 24273280d9495..d988de5851cf5 100644 --- a/x-pack/plugins/dashboard_mode/public/plugin.ts +++ b/x-pack/plugins/dashboard_mode/public/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trimLeft } from 'lodash'; +import { trimStart } from 'lodash'; import { CoreSetup } from 'kibana/public'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import { @@ -19,7 +19,7 @@ function defaultUrl(defaultAppId: string) { } function dashboardAppIdPrefix() { - return trimLeft(createDashboardEditUrl(''), '/'); + return trimStart(createDashboardEditUrl(''), '/'); } function migratePath(currentHash: string, kibanaLegacy: KibanaLegacyStart) { diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts index 546dc6361826a..20b3292128a2f 100644 --- a/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts @@ -6,7 +6,7 @@ import { CoreSetup } from 'kibana/public'; import { $Keys } from 'utility-types'; -import { flatten, uniq } from 'lodash'; +import { flatten, uniqBy } from 'lodash'; import { setupGetFieldSuggestions } from './field'; import { setupGetValueSuggestions } from './value'; import { setupGetOperatorSuggestions } from './operator'; @@ -21,7 +21,7 @@ import { const cursorSymbol = '@kuery-cursor@'; const dedup = (suggestions: QuerySuggestion[]): QuerySuggestion[] => - uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); + uniqBy(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); export const KUERY_LANGUAGE_NAME = 'kuery'; diff --git a/x-pack/plugins/event_log/server/event_log_service.ts b/x-pack/plugins/event_log/server/event_log_service.ts index 6cfb16d126427..f7f915f1cf0ef 100644 --- a/x-pack/plugins/event_log/server/event_log_service.ts +++ b/x-pack/plugins/event_log/server/event_log_service.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { Observable } from 'rxjs'; import { LegacyClusterClient } from 'src/core/server'; 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 36a6bc0a926af..0339d0883dc46 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 @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { Observable } from 'rxjs'; import { LegacyClusterClient, diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index e41035e9365ce..2570d4540b6a6 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { Feature } from '../common/feature'; -const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue']; +const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const; interface FeatureCapabilities { [featureId: string]: Record; @@ -67,7 +67,7 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UICapabilities { return allFeatureCapabilities.reduce((acc, capabilities) => { - const mergableCapabilities: UICapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); + const mergableCapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); const mergedFeatureCapabilities = { ...mergableCapabilities, diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 648010eeeafeb..b0b8cf14ff699 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -59,7 +59,7 @@ export function registerExploreRoute({ error, 'body.error.root_cause', [] as Array<{ type: string; reason: string }> - ).find((cause) => { + ).find((cause: { type: string; reason: string }) => { return ( cause.reason.includes('Fielddata is disabled on text fields') || cause.reason.includes('No support for examining floating point') || diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js index e7afc8f12859c..a1eac5264bb6a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { get, every, any } from 'lodash'; +import { get, every, some } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar } from '@elastic/eui'; @@ -129,7 +129,7 @@ export const ilmSummaryExtension = (index, getUrlForApp) => { }; export const ilmFilterExtension = (indices) => { - const hasIlm = any(indices, (index) => index.ilm && index.ilm.managed); + const hasIlm = some(indices, (index) => index.ilm && index.ilm.managed); if (!hasIlm) { return []; } else { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 98bd3077670a7..5eb4eaf6e2ca1 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { @@ -13,44 +12,21 @@ import { TestBedConfig, findTestSubject, } from '../../../../../test_utils'; -// NOTE: We have to use the Home component instead of the TemplateList component because we depend -// upon react router to provide the name of the template to load in the detail panel. -import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { TemplateList } from '../../../public/application/sections/home/template_list'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { TemplateDeserialized } from '../../../common'; -import { WithAppDependencies, services, TestSubjects } from '../helpers'; +import { WithAppDependencies, TestSubjects } from '../helpers'; const testBedConfig: TestBedConfig = { - store: () => indexManagementStore(services as any), memoryRouter: { - initialEntries: [`/indices`], - componentRoutePath: `/:section(indices|templates)`, + initialEntries: [`/templates`], + componentRoutePath: `/templates/:templateName?`, }, doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); - -export interface IndexTemplatesTabTestBed extends TestBed { - findAction: (action: 'edit' | 'clone' | 'delete') => ReactWrapper; - actions: { - goToTemplatesList: () => void; - selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void; - clickReloadButton: () => void; - clickTemplateAction: ( - name: TemplateDeserialized['name'], - action: 'edit' | 'clone' | 'delete' - ) => void; - clickTemplateAt: (index: number) => void; - clickCloseDetailsButton: () => void; - clickActionMenu: (name: TemplateDeserialized['name']) => void; - toggleViewItem: (view: 'composable' | 'system') => void; - }; -} - -export const setup = async (): Promise => { - const testBed = await initTestBed(); +const initTestBed = registerTestBed(WithAppDependencies(TemplateList), testBedConfig); +const createActions = (testBed: TestBed) => { /** * Additional helpers */ @@ -64,11 +40,6 @@ export const setup = async (): Promise => { /** * User Actions */ - - const goToTemplatesList = () => { - testBed.find('templatesTab').simulate('click'); - }; - const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => { const tabs = ['summary', 'settings', 'mappings', 'aliases']; @@ -136,10 +107,8 @@ export const setup = async (): Promise => { }; return { - ...testBed, findAction, actions: { - goToTemplatesList, selectDetailsTab, clickReloadButton, clickTemplateAction, @@ -150,3 +119,14 @@ export const setup = async (): Promise => { }, }; }; + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + ...createActions(testBed), + }; +}; + +export type IndexTemplatesTabTestBed = TestBed & ReturnType; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index 2ff3743cd866c..fb3e16e5345cb 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -30,28 +30,15 @@ describe('Index Templates tab', () => { server.restore(); }); - beforeEach(async () => { - httpRequestsMockHelpers.setLoadIndicesResponse([]); - - await act(async () => { - testBed = await setup(); - }); - }); - describe('when there are no index templates', () => { - beforeEach(async () => { - const { actions, component } = testBed; - + test('should display an empty prompt', async () => { httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); await act(async () => { - actions.goToTemplatesList(); + testBed = await setup(); }); + const { exists, component } = testBed; component.update(); - }); - - test('should display an empty prompt', async () => { - const { exists } = testBed; expect(exists('sectionLoading')).toBe(false); expect(exists('emptyPrompt')).toBe(true); @@ -119,14 +106,12 @@ describe('Index Templates tab', () => { const legacyTemplates = [template4, template5, template6]; beforeEach(async () => { - const { actions, component } = testBed; - httpRequestsMockHelpers.setLoadTemplatesResponse({ templates, legacyTemplates }); await act(async () => { - actions.goToTemplatesList(); + testBed = await setup(); }); - component.update(); + testBed.component.update(); }); test('should list them in the table', async () => { @@ -151,6 +136,7 @@ describe('Index Templates tab', () => { composedOfString, priorityFormatted, 'M S A', // Mappings Settings Aliases badges + '', // Column of actions ]); }); @@ -192,8 +178,10 @@ describe('Index Templates tab', () => { ); }); - test('should have a button to create a new template', () => { + test('should have a button to create a template', () => { const { exists } = testBed; + // Both composable and legacy templates + expect(exists('createTemplateButton')).toBe(true); expect(exists('createLegacyTemplateButton')).toBe(true); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 07a27e2414aed..69d7a13edfcfb 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../common'; import { setupEnvironment, nextTick } from '../helpers'; import { @@ -369,7 +368,7 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { - isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, + isLegacy: false, isManaged: false, }, }; diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index 526b9fede2a67..d1700f0e611c0 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -9,7 +9,6 @@ export { BASE_PATH } from './base_path'; export { API_BASE_PATH } from './api_base_path'; export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters'; export * from './index_statuses'; -export { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from './index_templates'; export { UIM_APP_NAME, diff --git a/x-pack/plugins/index_management/common/constants/index_templates.ts b/x-pack/plugins/index_management/common/constants/index_templates.ts deleted file mode 100644 index 7696b3832c51e..0000000000000 --- a/x-pack/plugins/index_management/common/constants/index_templates.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * Up until the end of the 8.x release cycle we need to support both - * legacy and composable index template formats. This constant keeps track of whether - * we create legacy index template format by default in the UI. - */ -export const CREATE_LEGACY_TEMPLATE_BY_DEFAULT = true; diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts index 4ad428744deab..119d4e0c54edd 100644 --- a/x-pack/plugins/index_management/common/index.ts +++ b/x-pack/plugins/index_management/common/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PLUGIN, API_BASE_PATH, CREATE_LEGACY_TEMPLATE_BY_DEFAULT, BASE_PATH } from './constants'; +export { PLUGIN, API_BASE_PATH, BASE_PATH } from './constants'; export { getTemplateParameter } from './lib'; diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index 4e76a40ced524..6b1005b4faa05 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -7,9 +7,11 @@ export { deserializeDataStream, deserializeDataStreamList } from './data_stream_serialization'; export { - deserializeLegacyTemplateList, + deserializeTemplate, deserializeTemplateList, deserializeLegacyTemplate, + deserializeLegacyTemplateList, + serializeTemplate, serializeLegacyTemplate, } from './template_serialization'; diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 249881f668d9f..608a8b8aca294 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -13,7 +13,7 @@ import { const hasEntries = (data: object = {}) => Object.entries(data).length > 0; export function serializeTemplate(templateDeserialized: TemplateDeserialized): TemplateSerialized { - const { version, priority, indexPatterns, template, composedOf } = templateDeserialized; + const { version, priority, indexPatterns, template, composedOf, _meta } = templateDeserialized; return { version, @@ -21,6 +21,7 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T template, index_patterns: indexPatterns, composed_of: composedOf, + _meta, }; } @@ -34,6 +35,7 @@ export function deserializeTemplate( index_patterns: indexPatterns, template = {}, priority, + _meta, composed_of: composedOf, } = templateEs; const { settings } = template; @@ -46,6 +48,7 @@ export function deserializeTemplate( template, ilmPolicy: settings?.index?.lifecycle, composedOf, + _meta, _kbnMeta: { isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), }, diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index 006a2d9dea8f2..14318b5fa2a8d 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -21,6 +21,7 @@ export interface TemplateSerialized { composed_of?: string[]; version?: number; priority?: number; + _meta?: { [key: string]: any }; } /** @@ -43,6 +44,7 @@ export interface TemplateDeserialized { ilmPolicy?: { name: string; }; + _meta?: { [key: string]: any }; _kbnMeta: { isManaged: boolean; isLegacy?: boolean; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 05a5ed462d8f7..f9e6234e1415c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -43,10 +43,6 @@ export const ComponentTemplateList: React.FunctionComponent = ({ trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD); }, [trackMetric]); - if (data && data.length === 0) { - return ; - } - let content: React.ReactNode; if (isLoading) { @@ -67,6 +63,8 @@ export const ComponentTemplateList: React.FunctionComponent = ({ history={history as ScopedHistory} /> ); + } else if (data && data.length === 0) { + content = ; } else if (error) { content = ; } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.scss b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.scss new file mode 100644 index 0000000000000..51e8a829e81b1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.scss @@ -0,0 +1,34 @@ + + +/** + * [1] Will center vertically the empty search result + */ + +$heightHeader: $euiSizeL * 2; + +.componentTemplates { + @include euiBottomShadowFlat; + height: 100%; + + &__header { + height: $heightHeader; + + .euiFormControlLayout { + max-width: initial; + } + } + + &__searchBox { + border-bottom: $euiBorderThin; + box-shadow: none; + max-width: initial; + } + + &__listWrapper { + height: calc(100% - #{$heightHeader}); + + &--is-empty { + display: flex; // [1] + } + } +} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx new file mode 100644 index 0000000000000..64c7cd400ba0d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import classNames from 'classnames'; +import React, { useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { ComponentTemplateListItem } from '../../../../../common'; +import { FilterListButton } from './components'; +import { ComponentTemplatesList } from './component_templates_list'; +import { Props as ComponentTemplatesListItemProps } from './component_templates_list_item'; + +import './component_templates.scss'; + +interface Props { + isLoading: boolean; + components: ComponentTemplateListItem[]; + listItemProps: Omit; +} + +interface Filters { + [key: string]: { name: string; checked: 'on' | 'off' }; +} + +function fuzzyMatch(searchValue: string, text: string) { + const pattern = `.*${searchValue.split('').join('.*')}.*`; + const regex = new RegExp(pattern); + return regex.test(text); +} + +const i18nTexts = { + filters: { + settings: i18n.translate( + 'xpack.idxMgmt.componentTemplatesSelector.filters.indexSettingsLabel', + { defaultMessage: 'Index settings' } + ), + mappings: i18n.translate('xpack.idxMgmt.componentTemplatesSelector.filters.mappingsLabel', { + defaultMessage: 'Mappings', + }), + aliases: i18n.translate('xpack.idxMgmt.componentTemplatesSelector.filters.aliasesLabel', { + defaultMessage: 'Aliases', + }), + }, + searchBoxPlaceholder: i18n.translate( + 'xpack.idxMgmt.componentTemplatesSelector.searchBox.placeholder', + { + defaultMessage: 'Search components', + } + ), +}; + +const getInitialFilters = (): Filters => ({ + settings: { + name: i18nTexts.filters.settings, + checked: 'off', + }, + mappings: { + name: i18nTexts.filters.mappings, + checked: 'off', + }, + aliases: { + name: i18nTexts.filters.aliases, + checked: 'off', + }, +}); + +export const ComponentTemplates = ({ isLoading, components, listItemProps }: Props) => { + const [searchValue, setSearchValue] = useState(''); + + const [filters, setFilters] = useState(getInitialFilters); + + const filteredComponents = useMemo(() => { + if (isLoading) { + return []; + } + + return components.filter((component) => { + if (filters.settings.checked === 'on' && !component.hasSettings) { + return false; + } + if (filters.mappings.checked === 'on' && !component.hasMappings) { + return false; + } + if (filters.aliases.checked === 'on' && !component.hasAliases) { + return false; + } + + if (searchValue.trim() === '') { + return true; + } + + const match = fuzzyMatch(searchValue, component.name); + return match; + }); + }, [isLoading, components, searchValue, filters]); + + const isSearchResultEmpty = filteredComponents.length === 0 && components.length > 0; + + if (isLoading) { + return null; + } + + const clearSearch = () => { + setSearchValue(''); + setFilters(getInitialFilters()); + }; + + const renderEmptyResult = () => { + return ( + + + + } + actions={ + + + + } + /> + ); + }; + + return ( +
+
+ + + { + setSearchValue(e.target.value); + }} + aria-label={i18nTexts.searchBoxPlaceholder} + className="componentTemplates__searchBox" + /> + + + + + +
+
+ {isSearchResultEmpty ? ( + renderEmptyResult() + ) : ( + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx new file mode 100644 index 0000000000000..0c64c38c8963f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { ComponentTemplateListItem } from '../../../../../common'; +import { + ComponentTemplatesListItem, + Props as ComponentTemplatesListItemProps, +} from './component_templates_list_item'; + +interface Props { + components: ComponentTemplateListItem[]; + listItemProps: Omit; +} + +export const ComponentTemplatesList = ({ components, listItemProps }: Props) => { + return ( + <> + {components.map((component) => ( + + ))} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.scss b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.scss new file mode 100644 index 0000000000000..b454d8697c5fc --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.scss @@ -0,0 +1,31 @@ +.componentTemplatesListItem { + background-color: white; + padding: $euiSizeM; + border-bottom: $euiBorderThin; + position: relative; + height: $euiSizeL * 2; + + &--selected { + &::before { + content: ''; + background-color: rgba(255, 255, 255, 0.7); + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: 1; + } + } + + &__contentIndicator { + flex-direction: row; + } + + &__checkIcon { + position: absolute; + right: $euiSize; + top: $euiSize; + z-index: 2; + } +} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx new file mode 100644 index 0000000000000..ad75c8dcbcc54 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_list_item.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import classNames from 'classnames'; +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiLink, + EuiIcon, + EuiToolTip, +} from '@elastic/eui'; + +import { ComponentTemplateListItem } from '../../../../../common'; +import { TemplateContentIndicator } from '../../shared'; + +import './component_templates_list_item.scss'; + +interface Action { + label: string; + icon: string; + handler: (component: ComponentTemplateListItem) => void; +} +export interface Props { + component: ComponentTemplateListItem; + isSelected?: boolean | ((component: ComponentTemplateListItem) => boolean); + onViewDetail: (component: ComponentTemplateListItem) => void; + actions?: Action[]; + dragHandleProps?: { [key: string]: any }; +} + +export const ComponentTemplatesListItem = ({ + component, + onViewDetail, + actions, + isSelected = false, + dragHandleProps, +}: Props) => { + const hasActions = actions && actions.length > 0; + const isSelectedValue = typeof isSelected === 'function' ? isSelected(component) : isSelected; + const isDraggable = Boolean(dragHandleProps); + + return ( +
+ + + + {isDraggable && ( + +
+ +
+
+ )} + + {/* {component.name} */} + onViewDetail(component)}>{component.name} + + + + +
+
+ + {/* Actions */} + {hasActions && !isSelectedValue && ( + + + {actions!.map((action, i) => ( + + + action.handler(component)} + data-test-subj="addPropertyButton" + aria-label={action.label} + /> + + + ))} + + + )} +
+ + {/* Check icon when selected */} + {isSelectedValue && ( + + )} +
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selection.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selection.tsx new file mode 100644 index 0000000000000..0a305eec19180 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selection.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiDragDropContext, EuiDraggable, EuiDroppable, euiDragDropReorder } from '@elastic/eui'; + +import { ComponentTemplateListItem } from '../../../../../common'; +import { + ComponentTemplatesListItem, + Props as ComponentTemplatesListItemProps, +} from './component_templates_list_item'; + +interface DraggableLocation { + droppableId: string; + index: number; +} + +interface Props { + components: ComponentTemplateListItem[]; + onReorder: (components: ComponentTemplateListItem[]) => void; + listItemProps: Omit; +} + +export const ComponentTemplatesSelection = ({ components, onReorder, listItemProps }: Props) => { + const onDragEnd = ({ + source, + destination, + }: { + source?: DraggableLocation; + destination?: DraggableLocation; + }) => { + if (source && destination) { + const items = euiDragDropReorder(components, source.index, destination.index); + onReorder(items); + } + }; + + return ( + + + {components.map((component, idx) => ( + + {(provided) => ( + + )} + + ))} + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss new file mode 100644 index 0000000000000..6abbbe65790e7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss @@ -0,0 +1,36 @@ +/* +[1] Height to align left and right column headers +*/ + +.componentTemplatesSelector { + height: 480px; + + &__selection { + @include euiBottomShadowFlat; + + padding: 0 $euiSize $euiSize; + color: $euiColorDarkShade; + + &--is-empty { + align-items: center; + justify-content: center; + } + + &__header { + background-color: $euiColorLightestShade; + border-bottom: $euiBorderThin; + color: $euiColorInk; + height: $euiSizeXXL; // [1] + line-height: $euiSizeXXL; // [1] + font-size: $euiSizeM; + margin-bottom: $euiSizeS; + margin-left: $euiSize * -1; + margin-right: $euiSize * -1; + padding-left: $euiSize; + + &__count { + font-weight: 600; + } + } + } +} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx new file mode 100644 index 0000000000000..af48c3c79379a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import classNames from 'classnames'; +import React, { useState, useEffect, useRef } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiEmptyPrompt, EuiLink, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { ComponentTemplateListItem } from '../../../../../common'; +import { SectionError, SectionLoading } from '../shared_imports'; +import { ComponentTemplateDetailsFlyout } from '../component_template_details'; +import { CreateButtonPopOver } from './components'; +import { ComponentTemplates } from './component_templates'; +import { ComponentTemplatesSelection } from './component_templates_selection'; +import { useApi } from '../component_templates_context'; + +import './component_templates_selector.scss'; + +interface Props { + onChange: (components: string[]) => void; + onComponentsLoaded: (components: ComponentTemplateListItem[]) => void; + defaultValue: string[]; + docUri: string; + emptyPrompt?: { + text?: string | JSX.Element; + showCreateButton?: boolean; + }; +} + +const i18nTexts = { + icons: { + view: i18n.translate('xpack.idxMgmt.componentTemplatesSelector.viewItemIconLabel', { + defaultMessage: 'View', + }), + select: i18n.translate('xpack.idxMgmt.componentTemplatesSelector.selectItemIconLabel', { + defaultMessage: 'Select', + }), + remove: i18n.translate('xpack.idxMgmt.componentTemplatesSelector.removeItemIconLabel', { + defaultMessage: 'Remove', + }), + }, +}; + +export const ComponentTemplatesSelector = ({ + onChange, + defaultValue, + onComponentsLoaded, + docUri, + emptyPrompt: { text, showCreateButton } = {}, +}: Props) => { + const { data: components, isLoading, error } = useApi().useLoadComponentTemplates(); + const [selectedComponent, setSelectedComponent] = useState(null); + const [componentsSelected, setComponentsSelected] = useState([]); + const isInitialized = useRef(false); + + const hasSelection = Object.keys(componentsSelected).length > 0; + const hasComponents = components && components.length > 0 ? true : false; + + useEffect(() => { + if (components) { + if ( + defaultValue.length > 0 && + componentsSelected.length === 0 && + isInitialized.current === false + ) { + // Once the components are loaded we check the ones selected + // from the defaultValue provided + const nextComponentsSelected = defaultValue + .map((name) => components.find((comp) => comp.name === name)) + .filter(Boolean) as ComponentTemplateListItem[]; + + setComponentsSelected(nextComponentsSelected); + onChange(nextComponentsSelected.map(({ name }) => name)); + isInitialized.current = true; + } else { + onChange(componentsSelected.map(({ name }) => name)); + } + } + }, [defaultValue, components, componentsSelected, onChange]); + + useEffect(() => { + if (!isLoading && !error) { + onComponentsLoaded(components ?? []); + } + }, [isLoading, error, components, onComponentsLoaded]); + + const onSelectionReorder = (reorderedComponents: ComponentTemplateListItem[]) => { + setComponentsSelected(reorderedComponents); + }; + + const renderLoading = () => ( + + + + ); + + const renderError = () => ( + + } + error={error!} + /> + ); + + const renderSelector = () => ( + + {/* Selection */} + + {hasSelection ? ( + <> +
+ + {componentsSelected.length} + + ), + }} + /> +
+
+ { + setSelectedComponent(component.name); + }, + actions: [ + { + label: i18nTexts.icons.remove, + icon: 'minusInCircle', + handler: (component: ComponentTemplateListItem) => { + setComponentsSelected((prev) => { + return prev.filter(({ name }) => component.name !== name); + }); + }, + }, + ], + }} + /> +
+ + ) : ( +
+ +
+ )} +
+ + {/* List of components */} + + { + setSelectedComponent(component.name); + }, + actions: [ + { + label: i18nTexts.icons.select, + icon: 'plusInCircle', + handler: (component: ComponentTemplateListItem) => { + setComponentsSelected((prev) => { + return [...prev, component]; + }); + }, + }, + ], + isSelected: (component: ComponentTemplateListItem) => { + return componentsSelected.find(({ name }) => component.name === name) !== undefined; + }, + }} + /> + +
+ ); + + const renderComponentDetails = () => { + if (!selectedComponent) { + return null; + } + + return ( + setSelectedComponent(null)} + componentTemplateName={selectedComponent} + /> + ); + }; + + if (isLoading) { + return renderLoading(); + } else if (error) { + return renderError(); + } else if (hasComponents) { + return ( + <> + {renderSelector()} + {renderComponentDetails()} + + ); + } + + // No components: render empty prompt + const emptyPromptBody = ( + +

+ {text ?? ( + + )} +
+ + + +

+
+ ); + return ( + + + + } + body={emptyPromptBody} + actions={showCreateButton ? : undefined} + data-test-subj="emptyPrompt" + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/create_button_popover.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/create_button_popover.tsx new file mode 100644 index 0000000000000..941e8ec362de2 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/create_button_popover.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiPopover, EuiButton, EuiContextMenu } from '@elastic/eui'; + +interface Props { + anchorPosition?: 'upCenter' | 'downCenter'; +} + +export const CreateButtonPopOver = ({ anchorPosition = 'upCenter' }: Props) => { + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + return ( + setIsPopOverOpen((prev) => !prev)} + > + +
+ } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition={anchorPosition} + repositionOnScroll + > + { + // console.log('Create component template...'); + }, + }, + { + name: i18n.translate( + 'xpack.idxMgmt.componentTemplatesFlyout.createComponentTemplateFromExistingButtonLabel', + { + defaultMessage: 'From existing index template', + } + ), + icon: 'symlink', + onClick: () => { + // console.log('Create component template from index template...'); + }, + }, + ], + }, + ]} + /> + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/filter_list_button.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/filter_list_button.tsx new file mode 100644 index 0000000000000..7236a385a704e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/filter_list_button.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFilterButton, EuiPopover, EuiFilterSelectItem } from '@elastic/eui'; + +interface Filter { + name: string; + checked: 'on' | 'off'; +} + +interface Props { + filters: Filters; + onChange(filters: Filters): void; +} + +export interface Filters { + [key: string]: Filter; +} + +export function FilterListButton({ onChange, filters }: Props) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const activeFilters = Object.values(filters).filter((v) => (v as Filter).checked === 'on'); + + const onButtonClick = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const toggleFilter = (filter: string) => { + const previousValue = filters[filter].checked; + const nextValue = previousValue === 'on' ? 'off' : 'on'; + + onChange({ + ...filters, + [filter]: { + ...filters[filter], + checked: nextValue, + }, + }); + }; + + const button = ( + 0} + numActiveFilters={activeFilters.length} + data-test-subj="viewButton" + > + + + ); + + return ( + +
+ {Object.entries(filters).map(([filter, item], index) => ( + toggleFilter(filter)} + data-test-subj="filterItem" + > + {(item as Filter).name} + + ))} +
+
+ ); +} diff --git a/x-pack/plugins/apm/typings/lodash.mean.d.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/index.ts similarity index 67% rename from x-pack/plugins/apm/typings/lodash.mean.d.ts rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/index.ts index 0b9ca3f6914cb..999b2e64cf133 100644 --- a/x-pack/plugins/apm/typings/lodash.mean.d.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/index.ts @@ -4,7 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -declare module 'lodash.mean' { - function mean(numbers: Array): number; - export = mean; -} +export * from './create_button_popover'; +export * from './filter_list_button'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/index.ts new file mode 100644 index 0000000000000..261a3d50d4626 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/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 { ComponentTemplatesSelector } from './component_templates_selector'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx index c78d24f126e29..bfea8d39e1203 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx @@ -61,3 +61,5 @@ export const useComponentTemplatesContext = () => { } return ctx; }; + +export const useApi = () => useComponentTemplatesContext().api; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index 72e79a57ae413..52235502e33df 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -9,3 +9,5 @@ export { ComponentTemplatesProvider } from './component_templates_context'; export { ComponentTemplateList } from './component_template_list'; export { ComponentTemplateDetailsFlyout } from './component_template_details'; + +export * from './component_template_selector'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 4a8cf965adfb9..63fe127c6b2d7 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../shared_imports'; +import { ComponentTemplateListItem, ComponentTemplateDeserialized, Error } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants'; import { UseRequestHook, SendRequestHook } from './request'; @@ -15,7 +15,7 @@ export const getApi = ( trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void ) => { function useLoadComponentTemplates() { - return useRequest({ + return useRequest({ path: `${apiBasePath}/component_templates`, method: 'get', }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts index 97ffa4d875ecb..27ee2bb81caf1 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts @@ -15,13 +15,15 @@ import { useRequest as _useRequest, } from '../shared_imports'; -export type UseRequestHook = (config: UseRequestConfig) => UseRequestResponse; +export type UseRequestHook = ( + config: UseRequestConfig +) => UseRequestResponse; export type SendRequestHook = (config: SendRequestConfig) => Promise; -export const getUseRequest = (httpClient: HttpSetup): UseRequestHook => ( +export const getUseRequest = (httpClient: HttpSetup): UseRequestHook => ( config: UseRequestConfig ) => { - return _useRequest(httpClient, config); + return _useRequest(httpClient, config); }; export const getSendRequest = (httpClient: HttpSetup): SendRequestHook => ( diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index 4e56f4a8c9818..bd19c2004894c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -18,6 +18,7 @@ export { Error, useAuthorizationContext, NotAuthorizedSection, + Forms, } from '../../../../../../../src/plugins/es_ui_shared/public'; export { TabMappings, TabSettings, TabAliases } from '../shared'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts index b67a9c355e723..b0a76b828449c 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts @@ -12,3 +12,5 @@ export { StepSettingsContainer, CommonWizardSteps, } from './wizard_steps'; + +export { TemplateContentIndicator } from './template_content_indicator'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx similarity index 100% rename from x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx rename to x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx index 0d28ec4b50c9a..d71d72d873c8e 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx @@ -23,13 +23,13 @@ import { Forms } from '../../../../../shared_imports'; import { useJsonStep } from './use_json_step'; interface Props { - defaultValue: { [key: string]: any }; + defaultValue?: { [key: string]: any }; onChange: (content: Forms.Content) => void; esDocsBase: string; } export const StepAliases: React.FunctionComponent = React.memo( - ({ defaultValue, onChange, esDocsBase }) => { + ({ defaultValue = {}, onChange, esDocsBase }) => { const { jsonContent, setJsonContent, error } = useJsonStep({ defaultValue, onChange, diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx index a5953ea00a106..c8297e6f298b6 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx @@ -14,7 +14,7 @@ interface Props { } export const StepAliasesContainer: React.FunctionComponent = ({ esDocsBase }) => { - const { defaultValue, updateContent } = Forms.useContent('aliases'); + const { defaultValue, updateContent } = Forms.useContent('aliases'); return ( diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx index 2b9b689e17cb9..bbf7a04080a28 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx @@ -24,14 +24,14 @@ import { } from '../../../mappings_editor'; interface Props { - defaultValue: { [key: string]: any }; onChange: (content: Forms.Content) => void; - indexSettings?: IndexSettings; esDocsBase: string; + defaultValue?: { [key: string]: any }; + indexSettings?: IndexSettings; } export const StepMappings: React.FunctionComponent = React.memo( - ({ defaultValue, onChange, indexSettings, esDocsBase }) => { + ({ defaultValue = {}, onChange, indexSettings, esDocsBase }) => { const [mappings, setMappings] = useState(defaultValue); const onMappingsEditorUpdate = useCallback( diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx index 34e05d88c651d..38c4a85bbe0ff 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx @@ -14,7 +14,9 @@ interface Props { } export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => { - const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); + const { defaultValue, updateContent, getData } = Forms.useContent( + 'mappings' + ); return ( void; esDocsBase: string; + defaultValue?: { [key: string]: any }; } export const StepSettings: React.FunctionComponent = React.memo( - ({ defaultValue, onChange, esDocsBase }) => { + ({ defaultValue = {}, onChange, esDocsBase }) => { const { jsonContent, setJsonContent, error } = useJsonStep({ defaultValue, onChange, diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx index c540ddceb95c2..42be2c4b28c10 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx @@ -14,7 +14,9 @@ interface Props { } export const StepSettingsContainer = React.memo(({ esDocsBase }: Props) => { - const { defaultValue, updateContent } = Forms.useContent('settings'); + const { defaultValue, updateContent } = Forms.useContent( + 'settings' + ); return ( diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts index 897e86c99eca0..9b0eeb7d18f6e 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/index.ts @@ -12,4 +12,5 @@ export { StepMappingsContainer, StepSettingsContainer, CommonWizardSteps, + TemplateContentIndicator, } from './components'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts index b7e3e36e61814..d8baca2db78a0 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts @@ -5,4 +5,5 @@ */ export { StepLogisticsContainer } from './step_logistics_container'; +export { StepComponentContainer } from './step_components_container'; export { StepReviewContainer } from './step_review_container'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx new file mode 100644 index 0000000000000..01771f40f89ea --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { ComponentTemplateListItem } from '../../../../../common'; +import { Forms } from '../../../../shared_imports'; +import { ComponentTemplatesSelector } from '../../component_templates'; + +interface Props { + esDocsBase: string; + onChange: (content: Forms.Content) => void; + defaultValue?: string[]; +} + +const i18nTexts = { + description: ( + + ), +}; + +export const StepComponents = ({ defaultValue = [], onChange, esDocsBase }: Props) => { + const [state, setState] = useState<{ + isLoadingComponents: boolean; + components: ComponentTemplateListItem[]; + }>({ isLoadingComponents: true, components: [] }); + + const onComponentsLoaded = useCallback((components: ComponentTemplateListItem[]) => { + setState({ isLoadingComponents: false, components }); + }, []); + + const onComponentSelectionChange = useCallback( + (components: string[]) => { + onChange({ isValid: true, validate: async () => true, getData: () => components }); + }, + [onChange] + ); + + const showHeader = state.isLoadingComponents === true || state.components.length > 0; + const docUri = `${esDocsBase}/indices-component-template.html`; + + const renderHeader = () => { + if (!showHeader) { + return null; + } + + return ( + <> + + + +

+ +

+
+ + + + +

{i18nTexts.description}

+
+
+ + + + + + +
+ + + + ); + }; + + return ( +
+ {renderHeader()} + + +
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components_container.tsx new file mode 100644 index 0000000000000..b9b09bf0e3d9a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components_container.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { Forms } from '../../../../shared_imports'; +import { documentationService } from '../../../services/documentation'; +import { WizardContent } from '../template_form'; +import { StepComponents } from './step_components'; + +export const StepComponentContainer = () => { + const { defaultValue, updateContent } = Forms.useContent( + 'components' + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index d011b4b06546a..44ec4db0873f3 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -8,7 +8,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from ' import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useForm, Form, getUseField, getFormRow, Field, Forms } from '../../../../shared_imports'; +import { + useForm, + Form, + getUseField, + getFormRow, + Field, + Forms, + JsonEditorField, +} from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; @@ -47,6 +55,15 @@ const fieldsMeta = { }), testSubject: 'orderField', }, + priority: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityTitle', { + defaultMessage: 'Merge priority', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityDescription', { + defaultMessage: 'The merge priority when multiple templates match an index.', + }), + testSubject: 'priorityField', + }, version: { title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionTitle', { defaultMessage: 'Version', @@ -62,20 +79,26 @@ interface Props { defaultValue: { [key: string]: any }; onChange: (content: Forms.Content) => void; isEditing?: boolean; + isLegacy?: boolean; } export const StepLogistics: React.FunctionComponent = React.memo( - ({ defaultValue, isEditing, onChange }) => { + ({ defaultValue, isEditing = false, onChange, isLegacy = false }) => { const { form } = useForm({ schema: schemas.logistics, defaultValue, options: { stripEmptyFields: false }, }); + /** + * When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state + * and we can display the form errors on top of the forms if there are any. + */ + const validate = async () => { + return (await form.submit()).isValid; + }; + useEffect(() => { - const validate = async () => { - return (await form.submit()).isValid; - }; onChange({ isValid: form.isValid, validate, @@ -83,10 +106,22 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps - const { name, indexPatterns, order, version } = fieldsMeta; + useEffect(() => { + const subscription = form.subscribe(({ data, isValid }) => { + onChange({ + isValid, + validate, + getData: data.format, + }); + }); + return subscription.unsubscribe; + }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps + + const { name, indexPatterns, order, priority, version } = fieldsMeta; return ( -
+ <> + {/* Header */} @@ -114,46 +149,106 @@ export const StepLogistics: React.FunctionComponent = React.memo( + - {/* Name */} - - - - {/* Index patterns */} - - - - {/* Order */} - - - - {/* Version */} - - - - + +
+ {/* Name */} + + + + + {/* Index patterns */} + + + + + {/* Order */} + {isLegacy && ( + + + + )} + + {/* Priority */} + {isLegacy === false && ( + + + + )} + + {/* Version */} + + + + + {/* _meta */} + {isLegacy === false && ( + + + + } + > + + + )} +
+ ); } ); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics_container.tsx index 867ecff799858..68a3419499088 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics_container.tsx @@ -10,13 +10,19 @@ import { WizardContent } from '../template_form'; import { StepLogistics } from './step_logistics'; interface Props { + isLegacy?: boolean; isEditing?: boolean; } -export const StepLogisticsContainer = ({ isEditing = false }: Props) => { - const { defaultValue, updateContent } = Forms.useContent('logistics'); +export const StepLogisticsContainer = ({ isEditing, isLegacy }: Props) => { + const { defaultValue, updateContent } = Forms.useContent('logistics'); return ( - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx index 7f301b0a9c282..880c7fbd7f23c 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx @@ -22,10 +22,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { serializers } from '../../../../shared_imports'; -import { - serializeLegacyTemplate, - serializeTemplate, -} from '../../../../../common/lib/template_serialization'; +import { serializeLegacyTemplate, serializeTemplate } from '../../../../../common/lib'; import { TemplateDeserialized, getTemplateParameter } from '../../../../../common'; import { WizardSection } from '../template_form'; @@ -66,6 +63,9 @@ export const StepReview: React.FunctionComponent = React.memo( indexPatterns, version, order, + priority, + composedOf, + _meta, _kbnMeta: { isLegacy }, } = template!; @@ -96,6 +96,7 @@ export const StepReview: React.FunctionComponent = React.memo( + {/* Index patterns */} = React.memo( )} - - - - - {order ? order : } - + {/* Priority / Order */} + {isLegacy ? ( + <> + + + + + {order ? order : } + + + ) : ( + <> + + + + + {priority ? priority : } + + + )} + {/* Version */} = React.memo( {version ? version : } + + {/* components */} + {isLegacy !== true && ( + <> + + + + + {composedOf && composedOf.length > 0 ? ( + composedOf.length > 1 ? ( + +
    + {composedOf.map((component: string, i: number) => { + return ( +
  • + + {component} + +
  • + ); + })} +
+
+ ) : ( + composedOf.toString() + ) + ) : ( + + )} +
+ + )}
+ {/* Index settings */} = React.memo( {getDescriptionText(serializedSettings)} + + {/* Mappings */} = React.memo( {getDescriptionText(serializedMappings)} + + {/* Aliases */} = React.memo( {getDescriptionText(serializedAliases)} + + {/* Metadata (optional) */} + {isLegacy !== true && _meta && ( + <> + + + + + {JSON.stringify(_meta, null, 2)} + + + )}
@@ -181,7 +255,8 @@ export const StepReview: React.FunctionComponent = React.memo( ); const RequestTab = () => { - const endpoint = `PUT _template/${name || ''}`; + const esApiEndpoint = isLegacy ? '_template' : '_index_template'; + const endpoint = `PUT ${esApiEndpoint}/${name || ''}`; const templateString = JSON.stringify(serializedTemplate, null, 2); const request = `${endpoint}\n${templateString}`; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 8a2c991aea8d0..269ad94251074 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -8,10 +8,10 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer } from '@elastic/eui'; -import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common'; +import { TemplateDeserialized } from '../../../../common'; import { serializers, Forms } from '../../../shared_imports'; import { SectionError } from '../section_error'; -import { StepLogisticsContainer, StepReviewContainer } from './steps'; +import { StepLogisticsContainer, StepComponentContainer, StepReviewContainer } from './steps'; import { CommonWizardSteps, StepSettingsContainer, @@ -28,12 +28,14 @@ interface Props { clearSaveError: () => void; isSaving: boolean; saveError: any; + isLegacy?: boolean; defaultValue?: TemplateDeserialized; isEditing?: boolean; } export interface WizardContent extends CommonWizardSteps { logistics: Omit; + components: TemplateDeserialized['composedOf']; } export type WizardSection = keyof WizardContent | 'review'; @@ -45,6 +47,12 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { defaultMessage: 'Logistics', }), }, + components: { + id: 'components', + label: i18n.translate('xpack.idxMgmt.templateForm.steps.componentsStepName', { + defaultMessage: 'Components', + }), + }, settings: { id: 'settings', label: i18n.translate('xpack.idxMgmt.templateForm.steps.settingsStepName', { @@ -72,9 +80,18 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { }; export const TemplateForm = ({ - defaultValue = { + defaultValue, + isEditing, + isSaving, + isLegacy = false, + saveError, + clearSaveError, + onSave, +}: Props) => { + const indexTemplate = defaultValue ?? { name: '', indexPatterns: [], + composedOf: [], template: { settings: {}, mappings: {}, @@ -82,26 +99,23 @@ export const TemplateForm = ({ }, _kbnMeta: { isManaged: false, - isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, + isLegacy, }, - }, - isEditing, - isSaving, - saveError, - clearSaveError, - onSave, -}: Props) => { + }; + const { template: { settings, mappings, aliases }, + composedOf, _kbnMeta, ...logistics - } = defaultValue; + } = indexTemplate; const wizardDefaultValue: WizardContent = { logistics, settings, mappings, aliases, + components: indexTemplate.composedOf, }; const i18nTexts = { @@ -139,6 +153,7 @@ export const TemplateForm = ({ ): TemplateDeserialized => ({ ...initialTemplate, ...wizardData.logistics, + composedOf: wizardData.components, template: { settings: wizardData.settings, mappings: wizardData.mappings, @@ -148,7 +163,7 @@ export const TemplateForm = ({ const onSaveTemplate = useCallback( async (wizardData: WizardContent) => { - const template = buildTemplateObject(defaultValue)(wizardData); + const template = buildTemplateObject(indexTemplate)(wizardData); // We need to strip empty string, otherwise if the "order" or "version" // are not set, they will be empty string and ES expect a number for those parameters. @@ -160,7 +175,7 @@ export const TemplateForm = ({ clearSaveError(); }, - [defaultValue, onSave, clearSaveError] + [indexTemplate, onSave, clearSaveError] ); return ( @@ -177,9 +192,15 @@ export const TemplateForm = ({ label={wizardSections.logistics.label} isRequired > - + + {indexTemplate._kbnMeta.isLegacy !== true && ( + + + + )} + @@ -193,7 +214,7 @@ export const TemplateForm = ({ - + ); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 9ff73b71adf50..5af3b4dd00c4f 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; import { FormSchema, @@ -28,6 +29,7 @@ const { startsWithField, indexPatternField, lowerCaseStringField, + isJsonField, } = fieldValidators; const { toInt } = fieldFormatters; const indexPatternInvalidCharacters = INVALID_INDEX_PATTERN_CHARS.join(' '); @@ -133,6 +135,13 @@ export const schemas: Record = { }), formatters: [toInt], }, + priority: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldPriorityLabel', { + defaultMessage: 'Priority (optional)', + }), + formatters: [toInt], + }, version: { type: FIELD_TYPES.NUMBER, label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldVersionLabel', { @@ -140,5 +149,43 @@ export const schemas: Record = { }), formatters: [toInt], }, + _meta: { + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.metaFieldEditorLabel', { + defaultMessage: '_meta field data (optional)', + }), + helpText: ( + {JSON.stringify({ arbitrary_data: 'anything_goes' })}, + }} + /> + ), + validations: [ + { + validator: isJsonField( + i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.metaFieldEditorJsonError', { + defaultMessage: 'The _meta field JSON is not valid.', + }), + { allowEmptyString: true } + ), + }, + ], + deserializer: (value: any) => { + if (value === '') { + return value; + } + return JSON.stringify(value, null, 2); + }, + serializer: (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, + }, }, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 1931884cf7306..5c249ee474b00 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -7,7 +7,7 @@ import React, { Component, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { all } from 'lodash'; +import { every } from 'lodash'; import { EuiBadge, EuiButton, @@ -66,11 +66,11 @@ export class IndexActionsContextMenu extends Component { unfreezeIndices, hasSystemIndex, } = this.props; - const allOpen = all(indexNames, (indexName) => { + const allOpen = every(indexNames, (indexName) => { return indexStatusByName[indexName] === INDEX_OPEN; }); - const allFrozen = all(indices, (index) => index.isFrozen); - const allUnfrozen = all(indices, (index) => !index.isFrozen); + const allFrozen = every(indices, (index) => index.isFrozen); + const allUnfrozen = every(indices, (index) => !index.isFrozen); const selectedIndexCount = indexNames.length; const items = []; if (!detailPanel && selectedIndexCount === 1) { diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts index dcaba319bb21a..156d792c26f1d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts @@ -5,4 +5,3 @@ */ export * from './filter_list_button'; -export * from './template_content_indicator'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx index ab4ce6a61a9b6..f85b14ea0d2d5 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx @@ -46,7 +46,7 @@ import { TabSummary } from '../../template_details/tabs'; interface Props { template: { name: string; isLegacy?: boolean }; onClose: () => void; - editTemplate: (name: string, isLegacy?: boolean) => void; + editTemplate: (name: string, isLegacy: boolean) => void; cloneTemplate: (name: string, isLegacy?: boolean) => void; reload: () => Promise; } @@ -290,7 +290,7 @@ export const LegacyTemplateDetails: React.FunctionComponent = ({ } ), icon: 'pencil', - onClick: () => editTemplate(templateName, isLegacy), + onClick: () => editTemplate(templateName, true), disabled: isManaged, }, { diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index edce05018ce39..99915c2b70e2a 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -19,7 +19,7 @@ import { useServices } from '../../../../../app_context'; interface Props { templates: TemplateListItem[]; reload: () => Promise; - editTemplate: (name: string, isLegacy?: boolean) => void; + editTemplate: (name: string, isLegacy: boolean) => void; cloneTemplate: (name: string, isLegacy?: boolean) => void; history: ScopedHistory; } @@ -150,8 +150,8 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ ), icon: 'pencil', type: 'icon', - onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { - editTemplate(name, isLegacy); + onClick: ({ name }: TemplateListItem) => { + editTemplate(name, true); }, enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, }, @@ -252,7 +252,10 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ iconType="plusInCircle" data-test-subj="createLegacyTemplateButton" key="createTemplateButton" - {...reactRouterNavigate(history, '/create_template')} + {...reactRouterNavigate(history, { + pathname: '/create_template', + search: 'legacy=true', + })} > - + ) : null; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx index 7c3f8c07a7e04..6a5328f76fb06 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx @@ -7,18 +7,27 @@ import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; + import { TemplateListItem } from '../../../../../../common'; import { TemplateDeleteModal } from '../../../../components'; -import { SendRequestResponse } from '../../../../../shared_imports'; -import { TemplateContentIndicator } from '../components'; +import { SendRequestResponse, reactRouterNavigate } from '../../../../../shared_imports'; +import { TemplateContentIndicator } from '../../../../components/shared'; interface Props { templates: TemplateListItem[]; reload: () => Promise; + editTemplate: (name: string) => void; + history: ScopedHistory; } -export const TemplateTable: React.FunctionComponent = ({ templates, reload }) => { +export const TemplateTable: React.FunctionComponent = ({ + templates, + reload, + history, + editTemplate, +}) => { const [templatesToDelete, setTemplatesToDelete] = useState< Array<{ name: string; isLegacy?: boolean }> >([]); @@ -80,13 +89,11 @@ export const TemplateTable: React.FunctionComponent = ({ templates, reloa sortable: true, }, { - field: 'hasMappings', name: i18n.translate('xpack.idxMgmt.templateList.table.overridesColumnTitle', { defaultMessage: 'Overrides', }), truncateText: true, - sortable: false, - render: (_, item) => ( + render: (item: TemplateListItem) => ( = ({ templates, reloa /> ), }, + { + name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', { + defaultMessage: 'Edit', + }), + isPrimary: true, + description: i18n.translate('xpack.idxMgmt.templateList.table.actionEditDecription', { + defaultMessage: 'Edit this template', + }), + icon: 'pencil', + type: 'icon', + onClick: ({ name }: TemplateListItem) => { + editTemplate(name); + }, + enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + }, + ], + }, ]; const pagination = { @@ -112,6 +141,20 @@ export const TemplateTable: React.FunctionComponent = ({ templates, reloa box: { incremental: true, }, + toolsRight: [ + + + , + ], }; return ( diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index f567b9835d53d..fb82f52968eb4 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -7,6 +7,8 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { useLocation } from 'react-router-dom'; +import { parse } from 'query-string'; import { TemplateForm } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; @@ -17,6 +19,8 @@ import { getTemplateDetailsLink } from '../../services/routing'; export const TemplateCreate: React.FunctionComponent = ({ history }) => { const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); + const search = parse(useLocation().search.substring(1)); + const isLegacy = Boolean(search.legacy); const onSave = async (template: TemplateDeserialized) => { const { name } = template; @@ -49,10 +53,17 @@ export const TemplateCreate: React.FunctionComponent = ({ h

- + {isLegacy ? ( + + ) : ( + + )}

@@ -61,6 +72,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h isSaving={isSaving} saveError={saveError} clearSaveError={clearSaveError} + isLegacy={isLegacy} />
diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index 2a895196189d0..8831fa2368f47 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -16,11 +16,19 @@ export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHas }; export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { - return encodeURI(`/edit_template/${encodePathForReactRouter(name)}?legacy=${isLegacy === true}`); + let url = `/edit_template/${encodePathForReactRouter(name)}`; + if (isLegacy) { + url = `${url}?legacy=true`; + } + return encodeURI(url); }; export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { - return encodeURI(`/clone_template/${encodePathForReactRouter(name)}?legacy=${isLegacy === true}`); + let url = `/clone_template/${encodePathForReactRouter(name)}`; + if (isLegacy) { + url = `${url}?legacy=true`; + } + return encodeURI(url); }; export const decodePathFromReactRouter = (pathname: string): string => { diff --git a/x-pack/plugins/index_management/public/index.scss b/x-pack/plugins/index_management/public/index.scss index 0fbf8ea5036c5..02686c4f7d6f3 100644 --- a/x-pack/plugins/index_management/public/index.scss +++ b/x-pack/plugins/index_management/public/index.scss @@ -1,6 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - // Index management plugin styles // Prefix all styles with "ind" to avoid conflicts. diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 69cd07ba6dba0..ad221ae73fecf 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -29,7 +29,11 @@ export { serializers, } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; -export { getFormRow, Field } from '../../../../src/plugins/es_ui_shared/static/forms/components'; +export { + getFormRow, + Field, + JsonEditorField, +} from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { isJSON } from '../../../../src/plugins/es_ui_shared/static/validators/string'; diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts index 6c0fbe3dd6a65..9f8bce241ae69 100644 --- a/x-pack/plugins/index_management/server/client/elasticsearch.ts +++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts @@ -126,6 +126,20 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'GET', }); + dataManagement.getComposableIndexTemplate = ca({ + urls: [ + { + fmt: '/_index_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + dataManagement.saveComposableIndexTemplate = ca({ urls: [ { @@ -154,4 +168,18 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'DELETE', }); + + dataManagement.existsTemplate = ca({ + urls: [ + { + fmt: '/_index_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'HEAD', + }); }; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/lib.ts b/x-pack/plugins/index_management/server/routes/api/templates/lib.ts new file mode 100644 index 0000000000000..fc5719cc04d0c --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/templates/lib.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { serializeTemplate, serializeLegacyTemplate } from '../../../../common/lib'; +import { TemplateDeserialized, LegacyTemplateSerialized } from '../../../../common'; +import { CallAsCurrentUser } from '../../../types'; + +export const doesTemplateExist = async ({ + name, + callAsCurrentUser, + isLegacy, +}: { + name: string; + callAsCurrentUser: CallAsCurrentUser; + isLegacy?: boolean; +}) => { + if (isLegacy) { + return await callAsCurrentUser('indices.existsTemplate', { name }); + } + return await callAsCurrentUser('dataManagement.existsTemplate', { name }); +}; + +export const saveTemplate = async ({ + template, + callAsCurrentUser, + isLegacy, +}: { + template: TemplateDeserialized; + callAsCurrentUser: CallAsCurrentUser; + isLegacy?: boolean; +}) => { + const serializedTemplate = isLegacy + ? serializeLegacyTemplate(template) + : serializeTemplate(template); + + if (isLegacy) { + const { + order, + index_patterns, + version, + settings, + mappings, + aliases, + } = serializedTemplate as LegacyTemplateSerialized; + + return await callAsCurrentUser('indices.putTemplate', { + name: template.name, + order, + body: { + index_patterns, + version, + settings, + mappings, + aliases, + }, + }); + } + + return await callAsCurrentUser('dataManagement.saveComposableIndexTemplate', { + name: template.name, + body: serializedTemplate, + }); +}; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index e0d92b3800785..4b735c941be70 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; import { TemplateDeserialized } from '../../../../common'; -import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; +import { saveTemplate, doesTemplateExist } from './lib'; const bodySchema = templateSchema; @@ -18,22 +18,17 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) router.post( { path: addBasePath('/index_templates'), validate: { body: bodySchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; + const { callAsCurrentUser } = ctx.dataManagement!.client; const template = req.body as TemplateDeserialized; const { _kbnMeta: { isLegacy }, } = template; - if (!isLegacy) { - return res.badRequest({ body: 'Only legacy index templates can be created.' }); - } - - const serializedTemplate = serializeLegacyTemplate(template); - const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; - // Check that template with the same name doesn't already exist - const templateExists = await callAsCurrentUser('indices.existsTemplate', { + const templateExists = await doesTemplateExist({ name: template.name, + callAsCurrentUser, + isLegacy, }); if (templateExists) { @@ -51,17 +46,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) try { // Otherwise create new index template - const response = await callAsCurrentUser('indices.putTemplate', { - name: template.name, - order, - body: { - index_patterns, - version, - settings, - mappings, - aliases, - }, - }); + const response = await saveTemplate({ template, callAsCurrentUser, isLegacy }); return res.ok({ body: response }); } catch (e) { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index ae5f7802a8408..1d8645268dc25 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -6,9 +6,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { + deserializeTemplate, + deserializeTemplateList, deserializeLegacyTemplate, deserializeLegacyTemplateList, - deserializeTemplateList, } from '../../../../common/lib'; import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates'; import { RouteDependencies } from '../../../types'; @@ -18,20 +19,19 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) { router.get( { path: addBasePath('/index_templates'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; + const { callAsCurrentUser } = ctx.dataManagement!.client; const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); - const _legacyTemplates = await callAsCurrentUser('indices.getTemplate'); - const { index_templates: _templates } = await callAsCurrentUser('transport.request', { - path: '_index_template', - method: 'GET', - }); + const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); + const { index_templates: templatesEs } = await callAsCurrentUser( + 'dataManagement.getComposableIndexTemplates' + ); const legacyTemplates = deserializeLegacyTemplateList( - _legacyTemplates, + legacyTemplatesEs, managedTemplatePrefix ); - const templates = deserializeTemplateList(_templates, managedTemplatePrefix); + const templates = deserializeTemplateList(templatesEs, managedTemplatePrefix); const body = { templates, @@ -49,7 +49,7 @@ const paramsSchema = schema.object({ // Require the template format version (V1 or V2) to be provided as Query param const querySchema = schema.object({ - legacy: schema.maybe(schema.boolean()), + legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); export function registerGetOneRoute({ router, license, lib }: RouteDependencies) { @@ -60,25 +60,37 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) }, license.guardApiRoute(async (ctx, req, res) => { const { name } = req.params as TypeOf; - const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - - const { legacy } = req.query as TypeOf; + const { callAsCurrentUser } = ctx.dataManagement!.client; - if (!legacy) { - return res.badRequest({ body: 'Only index template version 1 can be fetched.' }); - } + const isLegacy = (req.query as TypeOf).legacy === 'true'; try { const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); - const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name }); - if (indexTemplateByName[name]) { - return res.ok({ - body: deserializeLegacyTemplate( - { ...indexTemplateByName[name], name }, - managedTemplatePrefix - ), - }); + if (isLegacy) { + const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name }); + + if (indexTemplateByName[name]) { + return res.ok({ + body: deserializeLegacyTemplate( + { ...indexTemplateByName[name], name }, + managedTemplatePrefix + ), + }); + } + } else { + const { + index_templates: indexTemplates, + } = await callAsCurrentUser('dataManagement.getComposableIndexTemplate', { name }); + + if (indexTemplates.length > 0) { + return res.ok({ + body: deserializeTemplate( + { ...indexTemplates[0].index_template, name }, + managedTemplatePrefix + ), + }); + } } return res.notFound(); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index 7e9c3174d0591..3055321d6b594 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -6,10 +6,10 @@ import { schema } from '@kbn/config-schema'; import { TemplateDeserialized } from '../../../../common'; -import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; +import { saveTemplate, doesTemplateExist } from './lib'; const bodySchema = templateSchema; const paramsSchema = schema.object({ @@ -23,23 +23,15 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) validate: { body: bodySchema, params: paramsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; + const { callAsCurrentUser } = ctx.dataManagement!.client; const { name } = req.params as typeof paramsSchema.type; const template = req.body as TemplateDeserialized; const { _kbnMeta: { isLegacy }, } = template; - if (!isLegacy) { - return res.badRequest({ body: 'Only legacy index template can be edited.' }); - } - - const serializedTemplate = serializeLegacyTemplate(template); - - const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; - // Verify the template exists (ES will throw 404 if not) - const doesExist = await callAsCurrentUser('indices.existsTemplate', { name }); + const doesExist = await doesTemplateExist({ name, callAsCurrentUser, isLegacy }); if (!doesExist) { return res.notFound(); @@ -47,17 +39,7 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) try { // Next, update index template - const response = await callAsCurrentUser('indices.putTemplate', { - name, - order, - body: { - index_patterns, - version, - settings, - mappings, - aliases, - }, - }); + const response = await saveTemplate({ template, callAsCurrentUser, isLegacy }); return res.ok({ body: response }); } catch (e) { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index 6ab28e9021123..f82ea8f3cf152 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -11,6 +11,7 @@ export const templateSchema = schema.object({ indexPatterns: schema.arrayOf(schema.string()), version: schema.maybe(schema.number()), order: schema.maybe(schema.number()), + priority: schema.maybe(schema.number()), template: schema.maybe( schema.object({ settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), @@ -18,6 +19,8 @@ export const templateSchema = schema.object({ mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })), }) ), + composedOf: schema.maybe(schema.arrayOf(schema.string())), + _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), ilmPolicy: schema.maybe( schema.object({ name: schema.maybe(schema.string()), diff --git a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap new file mode 100644 index 0000000000000..99ab129fc36e3 --- /dev/null +++ b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap @@ -0,0 +1,215 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Metrics UI Observability Homepage Functions createMetricsFetchData() should just work 1`] = ` +Object { + "appLink": "/app/metrics", + "series": Object { + "inboundTraffic": Object { + "coordinates": Array [ + Object { + "x": 1593630455000, + "y": 0, + }, + Object { + "x": 1593630755000, + "y": 3.5, + }, + Object { + "x": 1593631055000, + "y": 3.5, + }, + Object { + "x": 1593631355000, + "y": 8.5, + }, + Object { + "x": 1593631655000, + "y": 3.5, + }, + Object { + "x": 1593631955000, + "y": 2.5, + }, + Object { + "x": 1593632255000, + "y": 1.5, + }, + Object { + "x": 1593632555000, + "y": 1.5, + }, + Object { + "x": 1593632855000, + "y": 3.5, + }, + Object { + "x": 1593633155000, + "y": 2.5, + }, + Object { + "x": 1593633455000, + "y": 1.5, + }, + Object { + "x": 1593633755000, + "y": 1.5, + }, + Object { + "x": 1593634055000, + "y": 2.5, + }, + Object { + "x": 1593634355000, + "y": 0, + }, + Object { + "x": 1593634655000, + "y": 10.5, + }, + Object { + "x": 1593634955000, + "y": 5.5, + }, + Object { + "x": 1593635255000, + "y": 13.5, + }, + Object { + "x": 1593635555000, + "y": 9.5, + }, + Object { + "x": 1593635855000, + "y": 7.5, + }, + Object { + "x": 1593636155000, + "y": 3, + }, + Object { + "x": 1593636455000, + "y": 3.5, + }, + ], + "label": "Inbound traffic", + }, + "outboundTraffic": Object { + "coordinates": Array [ + Object { + "x": 1593630455000, + "y": 0, + }, + Object { + "x": 1593630755000, + "y": 4, + }, + Object { + "x": 1593631055000, + "y": 4, + }, + Object { + "x": 1593631355000, + "y": 9, + }, + Object { + "x": 1593631655000, + "y": 4, + }, + Object { + "x": 1593631955000, + "y": 2.5, + }, + Object { + "x": 1593632255000, + "y": 2, + }, + Object { + "x": 1593632555000, + "y": 2, + }, + Object { + "x": 1593632855000, + "y": 4, + }, + Object { + "x": 1593633155000, + "y": 3, + }, + Object { + "x": 1593633455000, + "y": 2, + }, + Object { + "x": 1593633755000, + "y": 2, + }, + Object { + "x": 1593634055000, + "y": 2.5, + }, + Object { + "x": 1593634355000, + "y": 1, + }, + Object { + "x": 1593634655000, + "y": 11, + }, + Object { + "x": 1593634955000, + "y": 6, + }, + Object { + "x": 1593635255000, + "y": 14, + }, + Object { + "x": 1593635555000, + "y": 10, + }, + Object { + "x": 1593635855000, + "y": 8, + }, + Object { + "x": 1593636155000, + "y": 3, + }, + Object { + "x": 1593636455000, + "y": 4, + }, + ], + "label": "Outbound traffic", + }, + }, + "stats": Object { + "cpu": Object { + "label": "CPU usage", + "type": "percent", + "value": 0.0015, + }, + "hosts": Object { + "label": "Hosts", + "type": "number", + "value": 2, + }, + "inboundTraffic": Object { + "label": "Inbound traffic", + "type": "bytesPerSecond", + "value": 3.5, + }, + "memory": Object { + "label": "Memory usage", + "type": "percent", + "value": 0.0015, + }, + "outboundTraffic": Object { + "label": "Outbound traffic", + "type": "bytesPerSecond", + "value": 3, + }, + }, + "title": "Metrics", +} +`; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index d0b4045949d3e..8d36262b55792 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -364,7 +364,7 @@ export const Expressions: React.FC = (props) => { = (props) => { = ({ const dateFormatter = useMemo(() => { const firstSeries = data ? first(data.series) : null; return firstSeries && firstSeries.rows.length > 0 - ? niceTimeFormatter([first(firstSeries.rows).timestamp, last(firstSeries.rows).timestamp]) + ? niceTimeFormatter([ + (first(firstSeries.rows) as any).timestamp, + (last(firstSeries.rows) as any).timestamp, + ]) : (value: number) => `${value}`; }, [data]); @@ -135,8 +138,8 @@ export const ExpressionChart: React.FC = ({ }), }; - const firstTimestamp = first(firstSeries.rows).timestamp; - const lastTimestamp = last(firstSeries.rows).timestamp; + const firstTimestamp = (first(firstSeries.rows) as any).timestamp; + const lastTimestamp = (last(firstSeries.rows) as any).timestamp; const dataDomain = calculateDomain(series, [metric], false); const domain = { max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom. diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts index 0e631b1e333d7..f46a7f3e5a5e4 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts @@ -14,8 +14,8 @@ export const transformMetricsExplorerData = ( ) => { const { criteria } = params; if (criteria && data) { - const firstSeries = first(data.series); - const series = firstSeries.rows.reduce((acc, row) => { + const firstSeries = first(data.series) as any; + const series = firstSeries.rows.reduce((acc: any, row: any) => { const { timestamp } = row; criteria.forEach((item, index) => { if (!acc[index]) { diff --git a/x-pack/plugins/infra/public/index.scss b/x-pack/plugins/infra/public/index.scss index 05e045c1bd53b..a3d74e3afebe3 100644 --- a/x-pack/plugins/infra/public/index.scss +++ b/x-pack/plugins/infra/public/index.scss @@ -1,6 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - /* Infra plugin styles */ .infra-container-element { diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts new file mode 100644 index 0000000000000..21946c7c5653a --- /dev/null +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from 'src/core/public/mocks'; +import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers'; +import { CoreStart } from 'kibana/public'; +import { InfraClientStartDeps, InfraClientStartExports } from './types'; +import moment from 'moment'; +import { FAKE_SNAPSHOT_RESPONSE } from './test_utils'; + +function setup() { + const core = coreMock.createStart(); + const mockedGetStartServices = jest.fn(() => { + const deps = {}; + return Promise.resolve([ + core as CoreStart, + deps as InfraClientStartDeps, + void 0 as InfraClientStartExports, + ]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>; + }); + return { core, mockedGetStartServices }; +} + +describe('Metrics UI Observability Homepage Functions', () => { + describe('createMetricsHasData()', () => { + it('should return true when true', async () => { + const { core, mockedGetStartServices } = setup(); + core.http.get.mockResolvedValue({ + status: { + indexFields: [], + logIndicesExist: false, + metricIndicesExist: true, + }, + }); + const hasData = createMetricsHasData(mockedGetStartServices); + const response = await hasData(); + expect(core.http.get).toHaveBeenCalledTimes(1); + expect(response).toBeTruthy(); + }); + it('should return false when false', async () => { + const { core, mockedGetStartServices } = setup(); + core.http.get.mockResolvedValue({ + status: { + indexFields: [], + logIndicesExist: false, + metricIndicesExist: false, + }, + }); + const hasData = createMetricsHasData(mockedGetStartServices); + const response = await hasData(); + expect(core.http.get).toHaveBeenCalledTimes(1); + expect(response).toBeFalsy(); + }); + }); + + describe('createMetricsFetchData()', () => { + it('should just work', async () => { + const { core, mockedGetStartServices } = setup(); + core.http.post.mockResolvedValue(FAKE_SNAPSHOT_RESPONSE); + const fetchData = createMetricsFetchData(mockedGetStartServices); + const endTime = moment(); + const startTime = endTime.clone().subtract(1, 'h'); + const bucketSize = '300s'; + const response = await fetchData({ + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + bucketSize, + }); + expect(core.http.post).toHaveBeenCalledTimes(1); + expect(core.http.post).toHaveBeenCalledWith('/api/metrics/snapshot', { + body: JSON.stringify({ + sourceId: 'default', + metrics: [{ type: 'cpu' }, { type: 'memory' }, { type: 'rx' }, { type: 'tx' }], + groupBy: [], + nodeType: 'host', + timerange: { + from: startTime.valueOf(), + to: endTime.valueOf(), + interval: '300s', + forceInterval: true, + ignoreLookback: true, + }, + }), + }); + expect(response).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts new file mode 100644 index 0000000000000..d10ad5dda5320 --- /dev/null +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.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 moment from 'moment'; +import { sum, isFinite, isNumber } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { MetricsFetchDataResponse, FetchDataParams } from '../../observability/public'; +import { + SnapshotRequest, + SnapshotMetricInput, + SnapshotNode, + SnapshotNodeResponse, +} from '../common/http_api/snapshot_api'; +import { SnapshotMetricType } from '../common/inventory_models/types'; +import { InfraClientCoreSetup } from './types'; +import { SourceResponse } from '../common/http_api/source_api'; + +export const createMetricsHasData = ( + getStartServices: InfraClientCoreSetup['getStartServices'] +) => async () => { + const [coreServices] = await getStartServices(); + const { http } = coreServices; + const results = await http.get('/api/metrics/source/default/metrics'); + return results.status.metricIndicesExist; +}; + +export const average = (values: number[]) => (values.length ? sum(values) / values.length : 0); + +export const combineNodesBy = ( + metric: SnapshotMetricType, + nodes: SnapshotNode[], + combinator: (values: number[]) => number +) => { + const values = nodes.reduce((acc, node) => { + const snapshotMetric = node.metrics.find((m) => m.name === metric); + if (snapshotMetric?.value != null && isFinite(snapshotMetric.value)) { + acc.push(snapshotMetric.value); + } + return acc; + }, [] as number[]); + return combinator(values); +}; + +interface CombinedRow { + values: number[]; + timestamp: number; +} + +export const combineNodeTimeseriesBy = ( + metric: SnapshotMetricType, + nodes: SnapshotNode[], + combinator: (values: number[]) => number +) => { + const combinedTimeseries = nodes.reduce((acc, node) => { + const snapshotMetric = node.metrics.find((m) => m.name === metric); + if (snapshotMetric && snapshotMetric.timeseries) { + snapshotMetric.timeseries.rows.forEach((row) => { + const combinedRow = acc.find((r) => r.timestamp === row.timestamp); + if (combinedRow) { + combinedRow.values.push(isNumber(row.metric_0) ? row.metric_0 : 0); + } else { + acc.push({ + timestamp: row.timestamp, + values: [isNumber(row.metric_0) ? row.metric_0 : 0], + }); + } + }); + } + return acc; + }, [] as CombinedRow[]); + return combinedTimeseries.map((row) => ({ x: row.timestamp, y: combinator(row.values) })); +}; + +export const createMetricsFetchData = ( + getStartServices: InfraClientCoreSetup['getStartServices'] +) => async ({ + startTime, + endTime, + bucketSize, +}: FetchDataParams): Promise => { + const [coreServices] = await getStartServices(); + const { http } = coreServices; + const snapshotRequest: SnapshotRequest = { + sourceId: 'default', + metrics: ['cpu', 'memory', 'rx', 'tx'].map((type) => ({ type })) as SnapshotMetricInput[], + groupBy: [], + nodeType: 'host', + timerange: { + from: moment(startTime).valueOf(), + to: moment(endTime).valueOf(), + interval: bucketSize, + forceInterval: true, + ignoreLookback: true, + }, + }; + + const results = await http.post('/api/metrics/snapshot', { + body: JSON.stringify(snapshotRequest), + }); + + const inboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.rxLabel', { + defaultMessage: 'Inbound traffic', + }); + + const outboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.txLabel', { + defaultMessage: 'Outbound traffic', + }); + + return { + title: i18n.translate('xpack.infra.observabilityHomepage.metrics.title', { + defaultMessage: 'Metrics', + }), + appLink: '/app/metrics', + stats: { + hosts: { + type: 'number', + label: i18n.translate('xpack.infra.observabilityHomepage.metrics.hostsLabel', { + defaultMessage: 'Hosts', + }), + value: results.nodes.length, + }, + cpu: { + type: 'percent', + label: i18n.translate('xpack.infra.observabilityHomepage.metrics.cpuLabel', { + defaultMessage: 'CPU usage', + }), + value: combineNodesBy('cpu', results.nodes, average), + }, + memory: { + type: 'percent', + label: i18n.translate('xpack.infra.observabilityHomepage.metrics.memoryLabel', { + defaultMessage: 'Memory usage', + }), + value: combineNodesBy('memory', results.nodes, average), + }, + inboundTraffic: { + type: 'bytesPerSecond', + label: inboundLabel, + value: combineNodesBy('rx', results.nodes, average), + }, + outboundTraffic: { + type: 'bytesPerSecond', + label: outboundLabel, + value: combineNodesBy('tx', results.nodes, average), + }, + }, + series: { + inboundTraffic: { + label: inboundLabel, + coordinates: combineNodeTimeseriesBy('rx', results.nodes, average), + }, + outboundTraffic: { + label: outboundLabel, + coordinates: combineNodeTimeseriesBy('tx', results.nodes, average), + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx index cc4b6967d34fb..c2b49c43281a9 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { compose } from 'lodash'; +import { flowRight } from 'lodash'; import React from 'react'; import { match as RouteMatch, Redirect, RouteComponentProps } from 'react-router-dom'; @@ -24,7 +24,7 @@ interface RedirectToLogsProps extends RedirectToLogsType { export const RedirectToLogs = ({ location, match }: RedirectToLogsProps) => { const sourceId = match.params.sourceId || 'default'; const filter = getFilterFromLocation(location); - const searchString = compose( + const searchString = flowRight( replaceLogFilterInQueryString(filter), replaceLogPositionInQueryString(getTimeFromLocation(location)), replaceSourceIdInQueryString(sourceId) diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 10320ebbe7609..37203084124f5 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { compose } from 'lodash'; +import { flowRight } from 'lodash'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; @@ -65,7 +65,7 @@ export const RedirectToNodeLogs = ({ const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; - const searchString = compose( + const searchString = flowRight( replaceLogFilterInQueryString(filter), replaceLogPositionInQueryString(getTimeFromLocation(location)), replaceSourceIdInQueryString(sourceId) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 538cd5f7d9525..3997a7eab44e8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -266,7 +266,7 @@ export const LegendControls = ({ fullWidth label={ { @@ -79,7 +79,7 @@ export const calculateSteppedGradientColor = ( return rule.color; } return color; - }, first(rules).color || defaultColor); + }, (first(rules) as any).color || defaultColor); }; export const calculateStepColor = ( @@ -106,7 +106,7 @@ export const calculateGradientColor = ( return defaultColor; } if (rules.length === 1) { - return last(rules).color; + return (last(rules) as any).color; } const { min, max } = bounds; const sortedRules = sortBy(rules, 'value'); @@ -116,8 +116,10 @@ export const calculateGradientColor = ( return rule; } return acc; - }, first(sortedRules)); - const endRule = sortedRules.filter((r) => r !== startRule).find((r) => r.value >= normValue); + }, first(sortedRules)) as any; + const endRule = sortedRules + .filter((r) => r !== startRule) + .find((r) => r.value >= normValue) as any; if (!endRule) { return startRule.color; } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts index a5515895a51a0..b56b409717cc6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts @@ -30,7 +30,7 @@ function findOrCreateGroupWithNodes( * then look for the group in it's sub groups. */ if (path.length === 2) { - const parentId = first(path).value; + const parentId = (first(path) as any).value; const existingParentGroup = groups.find((g) => g.id === parentId); if (isWaffleMapGroupWithGroups(existingParentGroup)) { const existingSubGroup = existingParentGroup.groups.find((g) => g.id === id); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts index 0b8773db2dddf..c2cde7eb15e95 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts @@ -44,7 +44,7 @@ export const getMaxMinTimestamp = (metric: NodeDetailsMetricData): [number, numb const lastRow = last(item.data); return acc.concat([(firstRow && firstRow.timestamp) || 0, (lastRow && lastRow.timestamp) || 0]); }, [] as number[]); - return [min(values), max(values)]; + return [min(values) as number, max(values) as number]; }; /** diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 2a218c1c78aa3..3802366fe2ac5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -77,7 +77,10 @@ export const MetricsExplorerChart = ({ const dateFormatter = useMemo( () => series.rows.length > 0 - ? niceTimeFormatter([first(series.rows).timestamp, last(series.rows).timestamp]) + ? niceTimeFormatter([ + (first(series.rows) as any).timestamp, + (last(series.rows) as any).timestamp, + ]) : (value: number) => `${value}`, [series.rows] ); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 1b28945320bb6..2dda664a7f675 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -19,6 +19,7 @@ import { InfraClientPluginClass, } from './types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; +import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers'; export class Plugin implements InfraClientPluginClass { constructor(_context: PluginInitializerContext) {} @@ -36,6 +37,12 @@ export class Plugin implements InfraClientPluginClass { hasData: getLogsHasDataFetcher(core.getStartServices), fetchData: getLogsOverviewDataFetcher(core.getStartServices), }); + + pluginsSetup.observability.dashboard.register({ + appName: 'infra_metrics', + hasData: createMetricsHasData(core.getStartServices), + fetchData: createMetricsFetchData(core.getStartServices), + }); } core.application.register({ diff --git a/x-pack/plugins/infra/public/test_utils/index.ts b/x-pack/plugins/infra/public/test_utils/index.ts new file mode 100644 index 0000000000000..3de4c40f47cc9 --- /dev/null +++ b/x-pack/plugins/infra/public/test_utils/index.ts @@ -0,0 +1,309 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const FAKE_SNAPSHOT_RESPONSE = { + nodes: [ + { + path: [{ value: 'host-01', label: 'host-01', ip: '192.168.1.10' }], + metrics: [ + { + name: 'memory', + value: 0.002, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'memory', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'cpu', + value: 0.002, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'cpu', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'rx', + value: 4, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'rx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 4 }, + { timestamp: 1593631055000, metric_0: 4 }, + { timestamp: 1593631355000, metric_0: 9 }, + { timestamp: 1593631655000, metric_0: 4 }, + { timestamp: 1593631955000, metric_0: 3 }, + { timestamp: 1593632255000, metric_0: 2 }, + { timestamp: 1593632555000, metric_0: 2 }, + { timestamp: 1593632855000, metric_0: 4 }, + { timestamp: 1593633155000, metric_0: 3 }, + { timestamp: 1593633455000, metric_0: 2 }, + { timestamp: 1593633755000, metric_0: 2 }, + { timestamp: 1593634055000, metric_0: 3 }, + { timestamp: 1593634355000, metric_0: 0 }, + { timestamp: 1593634655000, metric_0: 11 }, + { timestamp: 1593634955000, metric_0: 6 }, + { timestamp: 1593635255000, metric_0: 14 }, + { timestamp: 1593635555000, metric_0: 10 }, + { timestamp: 1593635855000, metric_0: 8 }, + { timestamp: 1593636155000, metric_0: 4 }, + { timestamp: 1593636455000, metric_0: 4 }, + ], + }, + }, + { + name: 'tx', + value: 3, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'tx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 5 }, + { timestamp: 1593631055000, metric_0: 5 }, + { timestamp: 1593631355000, metric_0: 10 }, + { timestamp: 1593631655000, metric_0: 5 }, + { timestamp: 1593631955000, metric_0: 3 }, + { timestamp: 1593632255000, metric_0: 3 }, + { timestamp: 1593632555000, metric_0: 3 }, + { timestamp: 1593632855000, metric_0: 5 }, + { timestamp: 1593633155000, metric_0: 4 }, + { timestamp: 1593633455000, metric_0: 3 }, + { timestamp: 1593633755000, metric_0: 3 }, + { timestamp: 1593634055000, metric_0: 3 }, + { timestamp: 1593634355000, metric_0: 2 }, + { timestamp: 1593634655000, metric_0: 12 }, + { timestamp: 1593634955000, metric_0: 7 }, + { timestamp: 1593635255000, metric_0: 15 }, + { timestamp: 1593635555000, metric_0: 11 }, + { timestamp: 1593635855000, metric_0: 9 }, + { timestamp: 1593636155000, metric_0: 4 }, + { timestamp: 1593636455000, metric_0: 5 }, + ], + }, + }, + ], + }, + { + path: [{ value: 'host-02', label: 'host-02', ip: '192.168.1.11' }], + metrics: [ + { + name: 'memory', + value: 0.001, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'memory', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'cpu', + value: 0.001, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'cpu', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'rx', + value: 3, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'rx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 3 }, + { timestamp: 1593631055000, metric_0: 3 }, + { timestamp: 1593631355000, metric_0: 8 }, + { timestamp: 1593631655000, metric_0: 3 }, + { timestamp: 1593631955000, metric_0: 2 }, + { timestamp: 1593632255000, metric_0: 1 }, + { timestamp: 1593632555000, metric_0: 1 }, + { timestamp: 1593632855000, metric_0: 3 }, + { timestamp: 1593633155000, metric_0: 2 }, + { timestamp: 1593633455000, metric_0: 1 }, + { timestamp: 1593633755000, metric_0: 1 }, + { timestamp: 1593634055000, metric_0: 2 }, + { timestamp: 1593634355000, metric_0: 0 }, + { timestamp: 1593634655000, metric_0: 10 }, + { timestamp: 1593634955000, metric_0: 5 }, + { timestamp: 1593635255000, metric_0: 13 }, + { timestamp: 1593635555000, metric_0: 9 }, + { timestamp: 1593635855000, metric_0: 7 }, + { timestamp: 1593636155000, metric_0: 2 }, + { timestamp: 1593636455000, metric_0: 3 }, + ], + }, + }, + { + name: 'tx', + value: 3, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'tx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 3 }, + { timestamp: 1593631055000, metric_0: 3 }, + { timestamp: 1593631355000, metric_0: 8 }, + { timestamp: 1593631655000, metric_0: 3 }, + { timestamp: 1593631955000, metric_0: 2 }, + { timestamp: 1593632255000, metric_0: 1 }, + { timestamp: 1593632555000, metric_0: 1 }, + { timestamp: 1593632855000, metric_0: 3 }, + { timestamp: 1593633155000, metric_0: 2 }, + { timestamp: 1593633455000, metric_0: 1 }, + { timestamp: 1593633755000, metric_0: 1 }, + { timestamp: 1593634055000, metric_0: 2 }, + { timestamp: 1593634355000, metric_0: 0 }, + { timestamp: 1593634655000, metric_0: 10 }, + { timestamp: 1593634955000, metric_0: 5 }, + { timestamp: 1593635255000, metric_0: 13 }, + { timestamp: 1593635555000, metric_0: 9 }, + { timestamp: 1593635855000, metric_0: 7 }, + { timestamp: 1593636155000, metric_0: 2 }, + { timestamp: 1593636455000, metric_0: 3 }, + ], + }, + }, + ], + }, + ], + interval: '300s', +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index f4dc7de51daee..e3d23d86c9f56 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -103,7 +103,7 @@ const getData = async ( const { nodes } = await snapshot.getNodes(esClient, options); return nodes.reduce((acc, n) => { - const nodePathItem = last(n.path); + const nodePathItem = last(n.path) as any; const m = first(n.metrics); if (m && m.value && m.timeseries) { const { timeseries } = m; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 445911878111f..1ef86d9e7eac4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -52,7 +52,7 @@ export const createInventoryMetricThresholdExecutor = ( ) ); - const inventoryItems = Object.keys(first(results)); + const inventoryItems = Object.keys(first(results) as any); for (const item of inventoryItems) { const alertInstance = services.alertInstanceFactory(`${item}::${alertId}`); // AND logic; all criteria must be across the threshold diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index ba43303bccf0b..b865454951cd2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -53,7 +53,7 @@ export const previewInventoryMetricThresholdAlert = async ({ ) ); - const inventoryItems = Object.keys(first(results)); + const inventoryItems = Object.keys(first(results) as any); const previewResults = inventoryItems.map((item) => { const isNoData = results.some((result) => result[item].isNoData); if (isNoData) { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 5782277e4f469..4c02593dd0095 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -33,8 +33,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s const config = source.configuration; const alertResults = await evaluateAlert(services.callCluster, params, config); - // Because each alert result has the same group definitions, just grab the groups from the first one. - const groups = Object.keys(first(alertResults)); + // Because each alert result has the same group definitions, just grap the groups from the first one. + const groups = Object.keys(first(alertResults) as any); for (const group of groups) { const alertInstance = services.alertInstanceFactory(`${group}::${alertId}`); @@ -58,7 +58,9 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s let reason; if (nextState === AlertStates.ALERT) { - reason = alertResults.map((result) => buildFiredAlertReason(result[group])).join('\n'); + reason = alertResults + .map((result) => buildFiredAlertReason(result[group] as any)) + .join('\n'); } if (alertOnNoData) { if (nextState === AlertStates.NO_DATA) { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 39db24684e8d3..0ecfa27d0f0a8 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -70,7 +70,7 @@ export const previewMetricThresholdAlert: ( // Get a date histogram using the bucket interval and the lookback interval try { const alertResults = await evaluateAlert(callCluster, params, config, timeframe); - const groups = Object.keys(first(alertResults)); + const groups = Object.keys(first(alertResults) as any); // Now determine how to interpolate this histogram based on the alert interval const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); @@ -90,7 +90,7 @@ export const previewMetricThresholdAlert: ( // buckets would have fired the alert. If the alert interval and bucket interval are the same, // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation // will skip some buckets or read some buckets more than once, depending on the differential - const numberOfResultBuckets = first(alertResults)[group].shouldFire.length; + const numberOfResultBuckets = (first(alertResults) as any)[group].shouldFire.length; const numberOfExecutionBuckets = Math.floor( numberOfResultBuckets / alertResultsPerExecution ); @@ -118,7 +118,8 @@ export const previewMetricThresholdAlert: ( ? await evaluateAlert(callCluster, params, config) : []; const numberOfGroups = - precalculatedNumberOfGroups ?? Math.max(Object.keys(first(currentAlertResults)).length, 1); + precalculatedNumberOfGroups ?? + Math.max(Object.keys(first(currentAlertResults) as any).length, 1); const estimatedTotalBuckets = (lookbackIntervalInSeconds / bucketIntervalInSeconds) * numberOfGroups; // The minimum number of slices is 2. In case we underestimate the total number of buckets @@ -156,7 +157,7 @@ export const previewMetricThresholdAlert: ( return a + b; }) ); - return zippedResult; + return zippedResult as any; } else throw e; } }; diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts index bb1c4c6632af0..317a7da95ce6b 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts @@ -127,7 +127,7 @@ export const getNodeMetrics = ( avg: null, })); } - const lastBucket = findLastFullBucket(nodeBuckets, options); + const lastBucket = findLastFullBucket(nodeBuckets, options) as any; return options.metrics.map((metric, index) => { const metricResult: SnapshotNodeMetric = { name: metric.type, diff --git a/x-pack/plugins/infra/server/routes/ip_to_hostname.ts b/x-pack/plugins/infra/server/routes/ip_to_hostname.ts index 28b7777c1d688..08ad266a22f9b 100644 --- a/x-pack/plugins/infra/server/routes/ip_to_hostname.ts +++ b/x-pack/plugins/infra/server/routes/ip_to_hostname.ts @@ -48,7 +48,7 @@ export const initIpToHostName = ({ framework }: InfraBackendLibs) => { body: { message: 'Host with matching IP address not found.' }, }); } - const hostDoc = first(hits.hits); + const hostDoc = first(hits.hits) as any; return response.ok({ body: { host: hostDoc._source.host.name } }); } catch ({ statusCode = 500, message = 'Unknown error occurred' }) { return response.customError({ diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index 7e3a30e1e6918..b2664d5a6d9fe 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -59,7 +59,7 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { ); const info = await getNodeInfo(framework, requestContext, configuration, nodeId, nodeType); - const cloudInstanceId = get(info, 'cloud.instance.id'); + const cloudInstanceId = get(info, 'cloud.instance.id'); const cloudMetricsMetadata = cloudInstanceId ? await getCloudMetricsMetadata( diff --git a/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts b/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts index 559fba0799987..2b65c42410723 100644 --- a/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts +++ b/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts @@ -14,7 +14,7 @@ export const createAfterKeyHandler = ( if (!response.aggregations) { return options; } - const newOptions = { ...options }; + const newOptions = { ...options } as any; const afterKey = afterKeySelector(response); set(newOptions, optionsAfterKeyPath, afterKey); return newOptions; diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index 97b5cca369298..3d3c91a4310f8 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -6,5 +6,5 @@ export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; -export const DEFAULT_REGISTRY_URL = 'https://epr.elastic.co'; +export const DEFAULT_REGISTRY_URL = 'https://epr-snapshot.ea-web.elastic.dev'; export const INDEX_PATTERN_PLACEHOLDER_SUFFIX = '-index_pattern_placeholder'; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index bf6a8de15182d..3ee3039e9e1c4 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -250,7 +250,6 @@ export enum IngestAssetType { } export enum DefaultPackages { - base = 'base', system = 'system', endpoint = 'endpoint', } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts index c52471ccfb4f5..0d1f72afa16f1 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts @@ -5,7 +5,9 @@ */ export interface ListWithKuery { - page: number; - perPage: number; + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: 'desc' | 'asc'; kuery?: string; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 6ebfd3f28fd9b..36b7d412bf276 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -13,6 +13,7 @@ export { useLink } from './use_link'; export { useKibanaLink } from './use_kibana_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination } from './use_pagination'; +export { useSorting } from './use_sorting'; export { useDebounce } from './use_debounce'; export * from './use_request'; export * from './use_input'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_sorting.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_sorting.tsx new file mode 100644 index 0000000000000..b00809249897b --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_sorting.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useState } from 'react'; +import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; + +export function useSorting(defaultSorting: CriteriaWithPagination['sort']) { + const [sorting, setSorting] = useState['sort']>(defaultSorting); + + return { + sorting, + setSorting, + }; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx index 70668c2856f98..849d7bfc63f34 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx @@ -31,7 +31,12 @@ export const StepSelectConfig: React.FunctionComponent<{ data: agentConfigsData, error: agentConfigsError, isLoading: isAgentConfigsLoading, - } = useGetAgentConfigs(); + } = useGetAgentConfigs({ + page: 1, + perPage: 1000, + sortField: 'name', + sortOrder: 'asc', + }); const agentConfigs = agentConfigsData?.items || []; const agentConfigsById = agentConfigs.reduce( (acc: { [key: string]: GetAgentConfigsResponseItem }, config) => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx index 19243084f6821..42d1075e2ee1f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx @@ -118,6 +118,7 @@ export const PackageConfigsTable: React.FunctionComponent = ({ (): EuiInMemoryTableProps['columns'] => [ { field: 'name', + sortable: true, name: i18n.translate( 'xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle', { @@ -137,6 +138,7 @@ export const PackageConfigsTable: React.FunctionComponent = ({ }, { field: 'packageTitle', + sortable: true, name: i18n.translate( 'xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle', { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 0a9daf0038aab..4e79bd4fa7997 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -17,6 +17,7 @@ import { EuiTableFieldDataColumnType, EuiTextColor, } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; @@ -27,6 +28,7 @@ import { useCapabilities, useGetAgentConfigs, usePagination, + useSorting, useLink, useConfig, useUrlParams, @@ -84,6 +86,10 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { : urlParams.kuery ?? '' ); const { pagination, pageSizeOptions, setPagination } = usePagination(); + const { sorting, setSorting } = useSorting({ + field: 'updated_at', + direction: 'desc', + }); const history = useHistory(); const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const setIsCreateAgentConfigFlyoutOpen = useCallback( @@ -106,6 +112,8 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { const { isLoading, data: agentConfigData, sendRequest } = useGetAgentConfigs({ page: pagination.currentPage, perPage: pagination.pageSize, + sortField: sorting?.field, + sortOrder: sorting?.direction, kuery: search, }); @@ -116,6 +124,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { > = [ { field: 'name', + sortable: true, name: i18n.translate('xpack.ingestManager.agentConfigList.nameColumnTitle', { defaultMessage: 'Name', }), @@ -158,6 +167,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { }, { field: 'updated_at', + sortable: true, name: i18n.translate('xpack.ingestManager.agentConfigList.updatedOnColumnTitle', { defaultMessage: 'Last updated on', }), @@ -240,6 +250,16 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { [createAgentConfigButton] ); + const onTableChange = (criteria: CriteriaWithPagination) => { + const newPagination = { + ...pagination, + currentPage: criteria.page.index + 1, + pageSize: criteria.page.size, + }; + setPagination(newPagination); + setSorting(criteria.sort); + }; + return ( {isCreateAgentConfigFlyoutOpen ? ( @@ -276,7 +296,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { - loading={isLoading} hasActions={true} noItemsMessage={ @@ -314,14 +334,8 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { totalItemCount: agentConfigData ? agentConfigData.total : 0, pageSizeOptions, }} - onChange={({ page }: { page: { index: number; size: number } }) => { - const newPagination = { - ...pagination, - currentPage: page.index + 1, - pageSize: page.size, - }; - setPagination(newPagination); - }} + sorting={{ sort: sorting }} + onChange={onTableChange} /> ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx index 450def54ba1d0..592ca7f7b8380 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx @@ -36,7 +36,10 @@ export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onCl agent.config_id ); - const agentConfigsRequest = useGetAgentConfigs(); + const agentConfigsRequest = useGetAgentConfigs({ + page: 1, + perPage: 1000, + }); const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 98de9ac217af9..9819a4fa5d750 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -119,8 +119,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { }, mappings: { properties: { - id: { type: 'keyword' }, - name: { type: 'text' }, + name: { type: 'keyword' }, description: { type: 'text' }, namespace: { type: 'keyword' }, is_default: { type: 'boolean' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index ada35d1825069..bd00727714c33 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -143,10 +143,12 @@ class AgentConfigService { soClient: SavedObjectsClientContract, options: ListWithKuery ): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, kuery } = options; + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const agentConfigs = await soClient.find({ type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, // To ensure users don't need to know about SO data structure... @@ -273,7 +275,6 @@ class AgentConfigService { soClient, id, { - ...oldAgentConfig, package_configs: uniq( [...((oldAgentConfig.package_configs || []) as string[])].filter( (pkgConfigId) => !packageConfigIds.includes(pkgConfigId) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index c78a9ff8bb7b5..4420135aec952 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -12,20 +12,24 @@ import { AGENT_TYPE_EPHEMERAL, AGENT_POLLING_THRESHOLD_MS, } from '../../constants'; -import { AgentSOAttributes, Agent, AgentEventSOAttributes } from '../../types'; +import { AgentSOAttributes, Agent, AgentEventSOAttributes, ListWithKuery } from '../../types'; import { savedObjectToAgent } from './saved_objects'; import { escapeSearchQueryPhrase } from '../saved_object'; export async function listAgents( soClient: SavedObjectsClientContract, - options: { - page: number; - perPage: number; - kuery?: string; + options: ListWithKuery & { showInactive: boolean; } ) { - const { page, perPage, kuery, showInactive = false } = options; + const { + page = 1, + perPage = 20, + sortField = 'enrolled_at', + sortOrder = 'desc', + kuery, + showInactive = false, + } = options; const filters = []; @@ -49,10 +53,11 @@ export async function listAgents( const { saved_objects, total } = await soClient.find({ type: AGENT_SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, filter: _joinFilters(filters), - ..._getSortFields(), }); const agents: Agent[] = saved_objects.map(savedObjectToAgent); @@ -137,23 +142,6 @@ export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: }); } -function _getSortFields(sortOption?: string) { - switch (sortOption) { - case 'ASC': - return { - sortField: 'enrolled_at', - sortOrder: 'ASC', - }; - - case 'DESC': - default: - return { - sortField: 'enrolled_at', - sortOrder: 'DESC', - }; - } -} - function _joinFilters(filters: string[], operator = 'AND') { return filters.reduce((acc: string | undefined, filter) => { if (acc) { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/events.ts b/x-pack/plugins/ingest_manager/server/services/agents/events.ts index b6d87c9ca5b2f..55970607c74ab 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/events.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/events.ts @@ -31,7 +31,7 @@ export async function getAgentEvents( perPage, page, sortField: 'timestamp', - sortOrder: 'DESC', + sortOrder: 'desc', defaultSearchOperator: 'AND', search: agentId, searchFields: ['agent_id'], diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 0efb202eff532..016a2344cf532 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -61,7 +61,7 @@ async function getEventsCount(soClient: SavedObjectsClientContract, configId?: s perPage: 0, page: 1, sortField: 'timestamp', - sortOrder: 'DESC', + sortOrder: 'desc', defaultSearchOperator: 'AND', }); diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index 3b003f47eb6f9..02e2c8151fac7 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -29,7 +29,7 @@ export async function listEnrollmentApiKeys( page, perPage, sortField: 'created_at', - sortOrder: 'DESC', + sortOrder: 'desc', filter: kuery && kuery !== '' ? kuery.replace( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 635742c82f9a4..f5fec020bf5b4 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -2,7 +2,7 @@ exports[`tests loading base.yml: base.yml 1`] = ` { - "priority": 1, + "priority": 200, "index_patterns": [ "foo-*" ], @@ -105,7 +105,7 @@ exports[`tests loading base.yml: base.yml 1`] = ` exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` { - "priority": 1, + "priority": 200, "index_patterns": [ "foo-*" ], @@ -208,7 +208,7 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` exports[`tests loading system.yml: system.yml 1`] = ` { - "priority": 1, + "priority": 200, "index_patterns": [ "whatsthis-*" ], diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 06c07da6cd77a..2de378f717534 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -247,8 +247,11 @@ function getBaseTemplate( packageName: string ): IndexTemplate { return { - // This takes precedence over all index templates installed with the 'base' package - priority: 1, + // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*) + // if this number is lower than the ES value (which is 100) this template will never be applied when a data stream + // is created. I'm using 200 here to give some room for users to create their own template and fit it between the + // default and the one the ingest manager uses. + priority: 200, // To be completed with the correct index patterns index_patterns: [`${templateName}-*`], template: { diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index c886f4868ad30..5a7546bfee2e0 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -145,10 +145,12 @@ class PackageConfigService { soClient: SavedObjectsClientContract, options: ListWithKuery ): Promise<{ items: PackageConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, kuery } = options; + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const packageConfigs = await soClient.find({ type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, // To ensure users don't need to know about SO data structure... diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts index 2c8134d2e8f92..dc0f111680490 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts @@ -6,8 +6,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ListWithKuerySchema = schema.object({ - page: schema.number({ defaultValue: 1 }), - perPage: schema.number({ defaultValue: 20 }), + page: schema.maybe(schema.number({ defaultValue: 1 })), + perPage: schema.maybe(schema.number({ defaultValue: 20 })), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), kuery: schema.maybe(schema.string()), }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx similarity index 95% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx index 251a2ffe95212..d223653442819 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { usePipelineProcessorsContext } from '../context'; +import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context'; export const OnFailureProcessorsTitle: FunctionComponent = () => { const { links } = usePipelineProcessorsContext(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index a68e667f4ab43..341e15132d353 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -9,19 +9,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { useForm, Form, FormConfig } from '../../../shared_imports'; -import { Pipeline } from '../../../../common/types'; +import { Pipeline, Processor } from '../../../../common/types'; -import { - OnUpdateHandlerArg, - OnUpdateHandler, - SerializeResult, -} from '../pipeline_processors_editor'; +import './pipeline_form.scss'; + +import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; import { pipelineFormSchema } from './schema'; +import { PipelineForm as IPipelineForm } from './types'; export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; @@ -32,14 +31,15 @@ export interface PipelineFormProps { isEditing?: boolean; } +const defaultFormValue: Pipeline = Object.freeze({ + name: '', + description: '', + processors: [], + on_failure: [], +}); + export const PipelineForm: React.FunctionComponent = ({ - defaultValue = { - name: '', - description: '', - processors: [], - on_failure: [], - version: '', - }, + defaultValue = defaultFormValue, onSave, isSaving, saveError, @@ -50,34 +50,42 @@ export const PipelineForm: React.FunctionComponent = ({ const [isTestingPipeline, setIsTestingPipeline] = useState(false); - const processorStateRef = useRef(); + const { + processors: initialProcessors, + on_failure: initialOnFailureProcessors, + ...defaultFormValues + } = defaultValue; + + const [processorsState, setProcessorsState] = useState<{ + processors: Processor[]; + onFailure?: Processor[]; + }>({ + processors: initialProcessors, + onFailure: initialOnFailureProcessors, + }); - const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { - let override: SerializeResult | undefined; + const processorStateRef = useRef(); + const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { if (!isValid) { return; } if (processorStateRef.current) { - const processorsState = processorStateRef.current; - if (await processorsState.validate()) { - override = processorsState.getData(); - } else { - return; + const state = processorStateRef.current; + if (await state.validate()) { + onSave({ ...formData, ...state.getData() }); } } - - onSave({ ...formData, ...(override || {}) } as Pipeline); }; const handleTestPipelineClick = () => { setIsTestingPipeline(true); }; - const { form } = useForm({ + const { form } = useForm({ schema: pipelineFormSchema, - defaultValue, + defaultValue: defaultFormValues, onSubmit: handleSave, }); @@ -121,9 +129,12 @@ export const PipelineForm: React.FunctionComponent = ({ {/* All form fields */} { + setProcessorsState({ processors, onFailure }); + }} onEditorFlyoutOpen={onEditorFlyoutOpen} - initialProcessors={defaultValue.processors} - initialOnFailureProcessors={defaultValue.on_failure} + processors={processorsState.processors} + onFailure={processorsState.onFailure} onProcessorsUpdate={onProcessorsChangeHandler} hasVersion={Boolean(defaultValue.version)} isTestButtonDisabled={isTestingPipeline || form.isValid === false} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 52d1a77c1df6d..0e7a45e8d07b9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -6,17 +6,27 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Processor } from '../../../../common/types'; -import { FormDataProvider } from '../../../shared_imports'; -import { PipelineProcessorsEditor, OnUpdateHandler } from '../pipeline_processors_editor'; import { getUseField, getFormRow, Field, useKibana } from '../../../shared_imports'; +import { + PipelineProcessorsContextProvider, + GlobalOnFailureProcessorsEditor, + ProcessorsEditor, + OnUpdateHandler, + OnDoneLoadJsonHandler, +} from '../pipeline_processors_editor'; + +import { ProcessorsHeader } from './processors_header'; +import { OnFailureProcessorsTitle } from './on_failure_processors_title'; + interface Props { - initialProcessors: Processor[]; - initialOnFailureProcessors?: Processor[]; + processors: Processor[]; + onFailure?: Processor[]; + onLoadJson: OnDoneLoadJsonHandler; onProcessorsUpdate: OnUpdateHandler; hasVersion: boolean; isTestButtonDisabled: boolean; @@ -29,8 +39,9 @@ const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); export const PipelineFormFields: React.FunctionComponent = ({ - initialProcessors, - initialOnFailureProcessors, + processors, + onFailure, + onLoadJson, onProcessorsUpdate, isEditing, hasVersion, @@ -113,30 +124,37 @@ export const PipelineFormFields: React.FunctionComponent = ({ {/* Pipeline Processors Editor */} - - {({ processors, on_failure: onFailure }) => { - const processorProp = - typeof processors === 'string' && processors - ? JSON.parse(processors) - : initialProcessors ?? []; - - const onFailureProp = - typeof onFailure === 'string' && onFailure - ? JSON.parse(onFailure) - : initialOnFailureProcessors ?? []; - return ( - - ); - }} - + +
+ + + + + + + + + + + + + + + + + +
+
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx similarity index 84% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx index 6d1e2610b5c2b..5e5cddbd36b92 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx @@ -9,22 +9,26 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { usePipelineProcessorsContext } from '../context'; +import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context'; + +import { LoadFromJsonButton, OnDoneLoadJsonHandler } from '../pipeline_processors_editor'; export interface Props { onTestPipelineClick: () => void; isTestButtonDisabled: boolean; + onLoadJson: OnDoneLoadJsonHandler; } -export const ProcessorsTitleAndTestButton: FunctionComponent = ({ +export const ProcessorsHeader: FunctionComponent = ({ onTestPipelineClick, isTestButtonDisabled, + onLoadJson, }) => { const { links } = usePipelineProcessorsContext(); return ( @@ -55,6 +59,9 @@ export const ProcessorsTitleAndTestButton: FunctionComponent = ({ />
+ + + = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { + defaultMessage: 'Name is required.', + }) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { + defaultMessage: 'Description (optional)', + }), + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx deleted file mode 100644 index 5435f43a78acf..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ /dev/null @@ -1,138 +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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode } from '@elastic/eui'; - -import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; -import { parseJson, stringifyJson } from '../../lib'; - -const { emptyField, isJsonField } = fieldValidators; -const { toInt } = fieldFormatters; - -export const pipelineFormSchema: FormSchema = { - name: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { - defaultMessage: 'Name', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { - defaultMessage: 'Name is required.', - }) - ), - }, - ], - }, - description: { - type: FIELD_TYPES.TEXTAREA, - label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { - defaultMessage: 'Description (optional)', - }), - }, - processors: { - label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { - defaultMessage: 'Processors', - }), - helpText: ( - - {JSON.stringify([ - { - set: { - field: 'foo', - value: 'bar', - }, - }, - ])} - - ), - }} - /> - ), - serializer: parseJson, - deserializer: stringifyJson, - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.form.processorsRequiredError', { - defaultMessage: 'Processors are required.', - }) - ), - }, - { - validator: isJsonField( - i18n.translate('xpack.ingestPipelines.form.processorsJsonError', { - defaultMessage: 'The input is not valid.', - }) - ), - }, - ], - }, - on_failure: { - label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { - defaultMessage: 'Failure processors (optional)', - }), - helpText: ( - - {JSON.stringify([ - { - set: { - field: '_index', - value: 'failed-{{ _index }}', - }, - }, - ])} - - ), - }} - /> - ), - serializer: (value) => { - const result = parseJson(value); - // If an empty array was passed, strip out this value entirely. - if (!result.length) { - return undefined; - } - return result; - }, - deserializer: stringifyJson, - validations: [ - { - validator: (validationArg) => { - if (!validationArg.value) { - return; - } - return isJsonField( - i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { - defaultMessage: 'The input is not valid.', - }) - )(validationArg); - }, - }, - ], - }, - version: { - type: FIELD_TYPES.NUMBER, - label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { - defaultMessage: 'Version (optional)', - }), - formatters: [toInt], - }, -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts index bd74f09546ff4..aa52c14e61eae 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts @@ -7,3 +7,5 @@ import { Pipeline } from '../../../../common/types'; export type ReadProcessorsFunction = () => Pick; + +export type PipelineForm = Omit; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md new file mode 100644 index 0000000000000..d29af67d3179c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md @@ -0,0 +1,24 @@ +# Pipeline Processors Editor + +This component provides a way to visually build and manage an ingest +pipeline. + +# API + +## Editor components + +The top-level API consists of 3 pieces that enable the maximum amount +of flexibility for consuming code to determine overall layout. + +- PipelineProcessorsEditorContext +- ProcessorsEditor +- GlobalOnFailureProcessorsEditor + +The editor components must be wrapped inside of the context component +as this is where the shared processors state is contained. + +## Load JSON button + +This component is totally standalone. It gives users a button that +presents a modal for loading a pipeline. It does some basic +validation on the JSON to ensure that it is correct. diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index 7ad9aed3c44a4..cc3817d92d5e3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -6,7 +6,12 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; import { registerTestBed, TestBed } from '../../../../../../../test_utils'; -import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container'; +import { + PipelineProcessorsContextProvider, + Props, + ProcessorsEditor, + GlobalOnFailureProcessorsEditor, +} from '../'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -55,9 +60,16 @@ jest.mock('react-virtualized', () => { }; }); -const testBedSetup = registerTestBed(PipelineProcessorsEditor, { - doMountAsync: false, -}); +const testBedSetup = registerTestBed( + (props: Props) => ( + + + + ), + { + doMountAsync: false, + } +); export interface SetupResult extends TestBed { actions: ReturnType; @@ -146,10 +158,6 @@ const createActions = (testBed: TestBed) => { find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click'); }); }, - - toggleOnFailure() { - find('pipelineEditorOnFailureToggle').simulate('click'); - }, }; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx index 15121cc71c321..a4bbf840dff71 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -43,9 +43,9 @@ describe('Pipeline Editor', () => { }, onFlyoutOpen: jest.fn(), onUpdate, - isTestButtonDisabled: false, - onTestPipelineClick: jest.fn(), - esDocsBasePath: 'test', + links: { + esDocsBasePath: 'test', + }, }); }); @@ -57,13 +57,6 @@ describe('Pipeline Editor', () => { expect(arg.getData()).toEqual(testProcessors); }); - it('toggles the on-failure processors tree', () => { - const { actions, exists } = testBed; - expect(exists('pipelineEditorOnFailureTree')).toBe(false); - actions.toggleOnFailure(); - expect(exists('pipelineEditorOnFailureTree')).toBe(true); - }); - describe('processors', () => { it('adds a new processor', async () => { const { actions } = testBed; @@ -169,7 +162,6 @@ describe('Pipeline Editor', () => { it('moves to and from the global on-failure tree', async () => { const { actions } = testBed; - actions.toggleOnFailure(); await actions.addProcessor('onFailure', 'test', { if: '1 == 5' }); actions.moveProcessor('processors>0', 'dropButtonBelow-onFailure>0'); const [onUpdateResult1] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts index 2d512a6bfa2ed..de0621b187230 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -12,10 +12,10 @@ export { export { ProcessorsTree, ProcessorInfo, OnActionHandler } from './processors_tree'; -export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item/pipeline_processors_editor_item'; +export { PipelineProcessorsEditor } from './pipeline_processors_editor'; -export { ProcessorRemoveModal } from './processor_remove_modal'; +export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item'; -export { ProcessorsTitleAndTestButton } from './processors_title_and_test_button'; +export { ProcessorRemoveModal } from './processor_remove_modal'; -export { OnFailureProcessorsTitle } from './on_failure_processors_title'; +export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx new file mode 100644 index 0000000000000..482878d1bda58 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiButton } from '@elastic/eui'; + +import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider'; + +interface Props { + onDone: OnDoneLoadJsonHandler; +} + +const i18nTexts = { + buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel', { + defaultMessage: 'Load JSON', + }), +}; + +export const LoadFromJsonButton: FunctionComponent = ({ onDone }) => { + return ( + + {(openModal) => { + return ( + + {i18nTexts.buttonLabel} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts new file mode 100644 index 0000000000000..c1c49f251d518 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/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 { LoadFromJsonButton } from './button'; +export { OnDoneLoadJsonHandler } from './modal_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx new file mode 100644 index 0000000000000..2f4cdce1edd0b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: any) => fn, + }; +}); + +import { registerTestBed, TestBed } from '../../../../../../../../test_utils/testbed'; + +const setup = ({ onDone }: { onDone: OnDoneLoadJsonHandler }) => { + return registerTestBed( + () => ( + + {(openModal) => { + return ( + + ); + }} + + ), + { + memoryRouter: { + wrapComponent: false, + }, + } + )(); +}; + +describe('Load from JSON ModalProvider', () => { + let testBed: TestBed; + let onDone: jest.Mock; + + beforeEach(async () => { + onDone = jest.fn(); + testBed = await setup({ onDone }); + }); + + it('displays errors', () => { + const { find, exists } = testBed; + find('button').simulate('click'); + expect(exists('loadJsonConfirmationModal')); + const invalidPipeline = '{}'; + find('mockCodeEditor').simulate('change', { jsonString: invalidPipeline }); + find('confirmModalConfirmButton').simulate('click'); + const errorCallout = find('loadJsonConfirmationModal.errorCallOut'); + expect(errorCallout.text()).toContain('Please ensure the JSON is a valid pipeline object.'); + expect(onDone).toHaveBeenCalledTimes(0); + }); + + it('passes through a valid pipeline object', () => { + const { find, exists } = testBed; + find('button').simulate('click'); + expect(exists('loadJsonConfirmationModal')); + const validPipeline = JSON.stringify({ + processors: [{ set: { field: 'test', value: 123 } }, { badType1: null }, { badType2: 1 }], + on_failure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }); + find('mockCodeEditor').simulate('change', { jsonString: validPipeline }); + find('confirmModalConfirmButton').simulate('click'); + expect(!exists('loadJsonConfirmationModal')); + expect(onDone).toHaveBeenCalledTimes(1); + expect(onDone.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "on_failure": Array [ + Object { + "gsub": Object { + "field": "_index", + "pattern": "(.monitoring-\\\\w+-)6(-.+)", + "replacement": "$17$2", + }, + }, + ], + "processors": Array [ + Object { + "set": Object { + "field": "test", + "value": 123, + }, + }, + Object { + "badType1": null, + }, + Object { + "badType2": 1, + }, + ], + } + `); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx new file mode 100644 index 0000000000000..f183386d5927d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FunctionComponent, useRef, useState } from 'react'; +import { EuiConfirmModal, EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; + +import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../../shared_imports'; + +import { Processor } from '../../../../../../common/types'; + +import { deserialize } from '../../deserialize'; + +export type OnDoneLoadJsonHandler = (json: { + processors: Processor[]; + on_failure?: Processor[]; +}) => void; + +export interface Props { + onDone: OnDoneLoadJsonHandler; + children: (openModal: () => void) => React.ReactNode; +} + +const i18nTexts = { + modalTitle: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.modalTitle', { + defaultMessage: 'Load JSON', + }), + buttons: { + cancel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.cancel', { + defaultMessage: 'Cancel', + }), + confirm: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.confirm', { + defaultMessage: 'Load and overwrite', + }), + }, + editor: { + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.editor', { + defaultMessage: 'Pipeline object', + }), + }, + error: { + title: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.title', { + defaultMessage: 'Invalid pipeline', + }), + body: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.body', { + defaultMessage: 'Please ensure the JSON is a valid pipeline object.', + }), + }, +}; + +const defaultValue = {}; +const defaultValueRaw = JSON.stringify(defaultValue, null, 2); + +export const ModalProvider: FunctionComponent = ({ onDone, children }) => { + const [isModalVisible, setIsModalVisible] = useState(false); + const [isValidJson, setIsValidJson] = useState(true); + const [error, setError] = useState(); + const jsonContent = useRef['0']>({ + isValid: true, + validate: () => true, + data: { + format: () => defaultValue, + raw: defaultValueRaw, + }, + }); + const onJsonUpdate: OnJsonEditorUpdateHandler = (jsonUpdateData) => { + setIsValidJson(jsonUpdateData.validate()); + jsonContent.current = jsonUpdateData; + }; + return ( + <> + {children(() => setIsModalVisible(true))} + {isModalVisible ? ( + + { + setIsModalVisible(false); + }} + onConfirm={async () => { + try { + const json = jsonContent.current.data.format(); + const { processors, on_failure: onFailure } = json; + // This function will throw if it cannot parse the pipeline object + deserialize({ processors, onFailure }); + onDone(json as any); + setIsModalVisible(false); + } catch (e) { + setError(e); + } + }} + cancelButtonText={i18nTexts.buttons.cancel} + confirmButtonDisabled={!isValidJson} + confirmButtonText={i18nTexts.buttons.confirm} + maxWidth={600} + > +
+ + + + + + + {error && ( + <> + + {i18nTexts.error.body} + + + + )} + + +
+
+
+ ) : undefined} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx new file mode 100644 index 0000000000000..c89ff1d3d99ac --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, memo, useMemo } from 'react'; +import { ProcessorsTree } from '.'; +import { usePipelineProcessorsContext } from '../context'; + +import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from '../processors_reducer'; + +export interface Props { + stateSlice: typeof ON_FAILURE_STATE_SCOPE | typeof PROCESSOR_STATE_SCOPE; +} + +export const PipelineProcessorsEditor: FunctionComponent = memo( + function PipelineProcessorsEditor({ stateSlice }) { + const { + onTreeAction, + state: { editor, processors }, + } = usePipelineProcessorsContext(); + const baseSelector = useMemo(() => [stateSlice], [stateSlice]); + return ( + + ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx index 5bbea4b89b053..5cee5311c62a9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent, useState } from 'react'; import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiButtonIcon } from '@elastic/eui'; -import { editorItemMessages } from './messages'; +import { i18nTexts } from './i18n_texts'; interface Props { disabled: boolean; @@ -39,7 +39,7 @@ export const ContextMenu: FunctionComponent = (props) => { onDuplicate(); }} > - {editorItemMessages.duplicateButtonLabel} + {i18nTexts.duplicateButtonLabel} , showAddOnFailure ? ( = (props) => { onAddOnFailure(); }} > - {editorItemMessages.addOnFailureButtonLabel} + {i18nTexts.addOnFailureButtonLabel} ) : undefined, = (props) => { onDelete(); }} > - {editorItemMessages.deleteButtonLabel} + {i18nTexts.deleteButtonLabel} , ].filter(Boolean) as JSX.Element[]; @@ -82,7 +82,7 @@ export const ContextMenu: FunctionComponent = (props) => { disabled={disabled} onClick={() => setIsOpen((v) => !v)} iconType="boxesHorizontal" - aria-label={editorItemMessages.moreButtonAriaLabel} + aria-label={i18nTexts.moreButtonAriaLabel} /> } > diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts similarity index 98% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts index 913902d295503..ab080767b6029 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -export const editorItemMessages = { +export const i18nTexts = { moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', { defaultMessage: 'Move this processor', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 0fe804adaeb48..09c047d1d51b7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -25,7 +25,7 @@ import './pipeline_processors_editor_item.scss'; import { InlineTextInput } from './inline_text_input'; import { ContextMenu } from './context_menu'; -import { editorItemMessages } from './messages'; +import { i18nTexts } from './i18n_texts'; import { ProcessorInfo } from '../processors_tree'; export interface Handlers { @@ -52,7 +52,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( renderOnFailureHandlers, }) => { const { - state: { editor, processorsDispatch }, + state: { editor, processors }, } = usePipelineProcessorsContext(); const isDisabled = editor.mode.id !== 'idle'; @@ -115,7 +115,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( description: nextDescription, }; } - processorsDispatch({ + processors.dispatch({ type: 'updateProcessor', payload: { processor: { @@ -126,17 +126,17 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( }, }); }} - ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })} + ariaLabel={i18nTexts.processorTypeLabel({ type: processor.type })} text={description} - placeholder={editorItemMessages.descriptionPlaceholder} + placeholder={i18nTexts.descriptionPlaceholder} />
{!isInMoveMode && ( - + { @@ -151,12 +151,12 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( {!isInMoveMode && ( - + @@ -165,7 +165,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( - {editorItemMessages.cancelMoveButtonLabel} + {i18nTexts.cancelMoveButtonLabel} @@ -183,7 +183,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( editor.setMode({ id: 'removingProcessor', arg: { selector } }); }} onDuplicate={() => { - processorsDispatch({ + processors.dispatch({ type: 'duplicateProcessor', payload: { source: selector, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx index 9d284748a3d15..3eccda55fbb3a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx @@ -9,11 +9,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent, memo, useEffect } from 'react'; import { EuiButton, + EuiButtonEmpty, EuiHorizontalRule, EuiFlyout, EuiFlyoutHeader, - EuiTitle, EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; @@ -44,6 +46,11 @@ const addButtonLabel = i18n.translate( { defaultMessage: 'Add' } ); +const cancelButtonLabel = i18n.translate( + 'xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel', + { defaultMessage: 'Cancel' } +); + export const ProcessorSettingsForm: FunctionComponent = memo( ({ processor, form, isOnFailure, onClose, onOpen }) => { const { @@ -71,7 +78,7 @@ export const ProcessorSettingsForm: FunctionComponent = memo( return (
- + @@ -109,30 +116,19 @@ export const ProcessorSettingsForm: FunctionComponent = memo( {(arg: any) => { const { type } = arg; - let formContent: React.ReactNode | undefined; if (type?.length) { const formDescriptor = getProcessorFormDescriptor(type as any); if (formDescriptor?.FieldsComponent) { - formContent = ( + return ( <> ); - } else { - formContent = ; } - - return ( - <> - {formContent} - - {processor ? updateButtonLabel : addButtonLabel} - - - ); + return ; } // If the user has not yet defined a type, we do not show any settings fields @@ -140,6 +136,24 @@ export const ProcessorSettingsForm: FunctionComponent = memo( }} + + + + {cancelButtonLabel} + + + { + form.submit(); + }} + > + {processor ? updateButtonLabel : addButtonLabel} + + + + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts index 46e3d1c803fd5..87e6eb7f642a6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './processors_reducer'; + export enum DropSpecialLocations { top = 'TOP', bottom = 'BOTTOM', } + +export const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE]; +export const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE]; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx index fbc06f41208fe..ec864d31d1986 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx @@ -4,41 +4,242 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, Dispatch, FunctionComponent, useContext, useState } from 'react'; -import { EditorMode } from './types'; -import { ProcessorsDispatch } from './processors_reducer'; +import React, { + createContext, + Dispatch, + FunctionComponent, + useCallback, + useContext, + useEffect, + useMemo, + useState, + useRef, +} from 'react'; + +import { Processor } from '../../../../common/types'; + +import { EditorMode, FormValidityState, OnFormUpdateArg, OnUpdateHandlerArg } from './types'; + +import { + ProcessorsDispatch, + useProcessorsState, + State as ProcessorsState, + isOnFailureSelector, +} from './processors_reducer'; + +import { deserialize } from './deserialize'; + +import { serialize } from './serialize'; + +import { OnSubmitHandler, ProcessorSettingsForm } from './components/processor_settings_form'; + +import { OnActionHandler } from './components/processors_tree'; + +import { ProcessorRemoveModal } from './components'; + +import { getValue } from './utils'; interface Links { esDocsBasePath: string; } -const PipelineProcessorsContext = createContext<{ +interface ContextValue { links: Links; + onTreeAction: OnActionHandler; state: { - processorsDispatch: ProcessorsDispatch; + processors: { + state: ProcessorsState; + dispatch: ProcessorsDispatch; + }; editor: { mode: EditorMode; setMode: Dispatch; }; }; -}>({} as any); +} -interface Props { +const PipelineProcessorsContext = createContext({} as any); + +export interface Props { links: Links; - processorsDispatch: ProcessorsDispatch; + value: { + processors: Processor[]; + onFailure?: Processor[]; + }; + /** + * Give users a way to react to this component opening a flyout + */ + onFlyoutOpen: () => void; + onUpdate: (arg: OnUpdateHandlerArg) => void; } export const PipelineProcessorsContextProvider: FunctionComponent = ({ links, + value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, + onUpdate, + onFlyoutOpen, children, - processorsDispatch, }) => { + const initRef = useRef(false); const [mode, setMode] = useState({ id: 'idle' }); + const deserializedResult = useMemo( + () => + deserialize({ + processors: originalProcessors, + onFailure: originalOnFailureProcessors, + }), + [originalProcessors, originalOnFailureProcessors] + ); + const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); + + useEffect(() => { + if (initRef.current) { + processorsDispatch({ + type: 'loadProcessors', + payload: { + newState: deserializedResult, + }, + }); + } else { + initRef.current = true; + } + }, [deserializedResult, processorsDispatch]); + + const { onFailure: onFailureProcessors, processors } = processorsState; + + const [formState, setFormState] = useState({ + validate: () => Promise.resolve(true), + }); + + const onFormUpdate = useCallback<(arg: OnFormUpdateArg) => void>( + ({ isValid, validate }) => { + setFormState({ + validate: async () => { + if (isValid === undefined) { + return validate(); + } + return isValid; + }, + }); + }, + [setFormState] + ); + + useEffect(() => { + onUpdate({ + validate: async () => { + const formValid = await formState.validate(); + return formValid && mode.id === 'idle'; + }, + getData: () => + serialize({ + onFailure: onFailureProcessors, + processors, + }), + }); + }, [processors, onFailureProcessors, onUpdate, formState, mode]); + + const onSubmit = useCallback( + (processorTypeAndOptions) => { + switch (mode.id) { + case 'creatingProcessor': + processorsDispatch({ + type: 'addProcessor', + payload: { + processor: { ...processorTypeAndOptions }, + targetSelector: mode.arg.selector, + }, + }); + break; + case 'editingProcessor': + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...mode.arg.processor, + ...processorTypeAndOptions, + }, + selector: mode.arg.selector, + }, + }); + break; + default: + } + setMode({ id: 'idle' }); + }, + [processorsDispatch, mode, setMode] + ); + + const onCloseSettingsForm = useCallback(() => { + setMode({ id: 'idle' }); + setFormState({ validate: () => Promise.resolve(true) }); + }, [setFormState, setMode]); + + const onTreeAction = useCallback( + (action) => { + switch (action.type) { + case 'addProcessor': + setMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } }); + break; + case 'move': + setMode({ id: 'idle' }); + processorsDispatch({ + type: 'moveProcessor', + payload: action.payload, + }); + break; + case 'selectToMove': + setMode({ id: 'movingProcessor', arg: action.payload.info }); + break; + case 'cancelMove': + setMode({ id: 'idle' }); + break; + } + }, + [processorsDispatch, setMode] + ); + return ( {children} + + {mode.id === 'editingProcessor' || mode.id === 'creatingProcessor' ? ( + + ) : undefined} + {mode.id === 'removingProcessor' && ( + { + if (confirmed) { + processorsDispatch({ + type: 'removeProcessor', + payload: { selector }, + }); + } + setMode({ id: 'idle' }); + }} + /> + )} ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts new file mode 100644 index 0000000000000..9b7c2069fcddd --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { deserialize } from './deserialize'; + +describe('deserialize', () => { + it('tolerates certain bad values correctly', () => { + expect( + deserialize({ + processors: [ + { set: { field: 'test', value: 123 } }, + { badType1: null } as any, + { badType2: 1 } as any, + ], + onFailure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }) + ).toEqual({ + processors: [ + { + id: expect.any(String), + type: 'set', + options: { + field: 'test', + value: 123, + }, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType1', + options: {}, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType2', + options: {}, + }, + ], + onFailure: [ + { + id: expect.any(String), + type: 'gsub', + onFailure: undefined, + options: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }); + }); + + it('throws for unacceptable values', () => { + expect(() => { + deserialize({ + processors: [{ reallyBad: undefined } as any, 1 as any], + onFailure: [], + }); + }).toThrow('Invalid processor type'); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts index fa1d041bdaba3..1e9a97e189a5e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts @@ -22,12 +22,16 @@ const getProcessorType = (processor: Processor): string => { * See the definition of {@link ProcessorInternal} for why this works to extract the * processor type. */ - return Object.keys(processor)[0]!; + const type: unknown = Object.keys(processor)[0]; + if (typeof type !== 'string') { + throw new Error(`Invalid processor type. Received "${type}"`); + } + return type; }; const convertToPipelineInternalProcessor = (processor: Processor): ProcessorInternal => { const type = getProcessorType(processor); - const { on_failure: originalOnFailure, ...options } = processor[type]; + const { on_failure: originalOnFailure, ...options } = processor[type] ?? {}; const onFailure = originalOnFailure?.length ? convertProcessors(originalOnFailure) : (originalOnFailure as ProcessorInternal[] | undefined); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx new file mode 100644 index 0000000000000..7c62383024cfe --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx @@ -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. + */ + +import React, { FunctionComponent } from 'react'; + +import { PipelineProcessorsEditor } from '../components'; + +export const GlobalOnFailureProcessorsEditor: FunctionComponent = () => { + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts new file mode 100644 index 0000000000000..6c544b31df439 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/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 { GlobalOnFailureProcessorsEditor } from './global_on_failure_processors_editor'; +export { ProcessorsEditor } from './processors_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx new file mode 100644 index 0000000000000..108b22be43ca0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx @@ -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. + */ + +import React, { FunctionComponent } from 'react'; + +import { PipelineProcessorsEditor } from '../components'; + +export const ProcessorsEditor: FunctionComponent = () => { + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts index 58d6e492b85e5..89bc50fc0600a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PipelineProcessorsEditor, OnUpdateHandler } from './pipeline_processors_editor.container'; +export { PipelineProcessorsContextProvider, Props } from './context'; -export { OnUpdateHandlerArg } from './types'; +export { ProcessorsEditor, GlobalOnFailureProcessorsEditor } from './editors'; + +export { OnUpdateHandlerArg, OnUpdateHandler } from './types'; export { SerializeResult } from './serialize'; + +export { LoadFromJsonButton, OnDoneLoadJsonHandler } from './components'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx deleted file mode 100644 index 7257677c08fc2..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx +++ /dev/null @@ -1,74 +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, { FunctionComponent, useMemo } from 'react'; - -import { Processor } from '../../../../common/types'; - -import { deserialize } from './deserialize'; - -import { useProcessorsState } from './processors_reducer'; - -import { PipelineProcessorsContextProvider } from './context'; - -import { OnUpdateHandlerArg } from './types'; - -import { PipelineProcessorsEditor as PipelineProcessorsEditorUI } from './pipeline_processors_editor'; - -export interface Props { - value: { - processors: Processor[]; - onFailure?: Processor[]; - }; - onUpdate: (arg: OnUpdateHandlerArg) => void; - isTestButtonDisabled: boolean; - onTestPipelineClick: () => void; - esDocsBasePath: string; - /** - * Give users a way to react to this component opening a flyout - */ - onFlyoutOpen: () => void; -} - -export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; - -export const PipelineProcessorsEditor: FunctionComponent = ({ - value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, - onFlyoutOpen, - onUpdate, - isTestButtonDisabled, - esDocsBasePath, - onTestPipelineClick, -}) => { - const deserializedResult = useMemo( - () => - deserialize({ - processors: originalProcessors, - onFailure: originalOnFailureProcessors, - }), - // TODO: Re-add the dependency on the props and make the state set-able - // when new props come in so that this component will be controllable - [] // eslint-disable-line react-hooks/exhaustive-deps - ); - const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); - const { processors, onFailure } = processorsState; - - return ( - - - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx deleted file mode 100644 index 09e77c5107754..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx +++ /dev/null @@ -1,239 +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 { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent, useCallback, memo, useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch } from '@elastic/eui'; - -import './pipeline_processors_editor.scss'; - -import { - ProcessorsTitleAndTestButton, - OnFailureProcessorsTitle, - ProcessorsTree, - ProcessorRemoveModal, - OnActionHandler, - OnSubmitHandler, - ProcessorSettingsForm, -} from './components'; - -import { ProcessorInternal, OnUpdateHandlerArg, FormValidityState, OnFormUpdateArg } from './types'; - -import { - ON_FAILURE_STATE_SCOPE, - PROCESSOR_STATE_SCOPE, - isOnFailureSelector, -} from './processors_reducer'; - -const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE]; -const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE]; - -import { serialize } from './serialize'; -import { getValue } from './utils'; -import { usePipelineProcessorsContext } from './context'; - -export interface Props { - processors: ProcessorInternal[]; - onFailureProcessors: ProcessorInternal[]; - onUpdate: (arg: OnUpdateHandlerArg) => void; - isTestButtonDisabled: boolean; - onTestPipelineClick: () => void; - onFlyoutOpen: () => void; -} - -export const PipelineProcessorsEditor: FunctionComponent = memo( - function PipelineProcessorsEditor({ - processors, - onFailureProcessors, - onTestPipelineClick, - isTestButtonDisabled, - onUpdate, - onFlyoutOpen, - }) { - const { - state: { editor, processorsDispatch }, - } = usePipelineProcessorsContext(); - - const { mode: editorMode, setMode: setEditorMode } = editor; - - const [formState, setFormState] = useState({ - validate: () => Promise.resolve(true), - }); - - const onFormUpdate = useCallback<(arg: OnFormUpdateArg) => void>( - ({ isValid, validate }) => { - setFormState({ - validate: async () => { - if (isValid === undefined) { - return validate(); - } - return isValid; - }, - }); - }, - [setFormState] - ); - - const [showGlobalOnFailure, setShowGlobalOnFailure] = useState( - Boolean(onFailureProcessors.length) - ); - - useEffect(() => { - onUpdate({ - validate: async () => { - const formValid = await formState.validate(); - return formValid && editorMode.id === 'idle'; - }, - getData: () => - serialize({ - onFailure: showGlobalOnFailure ? onFailureProcessors : undefined, - processors, - }), - }); - }, [processors, onFailureProcessors, onUpdate, formState, editorMode, showGlobalOnFailure]); - - const onSubmit = useCallback( - (processorTypeAndOptions) => { - switch (editorMode.id) { - case 'creatingProcessor': - processorsDispatch({ - type: 'addProcessor', - payload: { - processor: { ...processorTypeAndOptions }, - targetSelector: editorMode.arg.selector, - }, - }); - break; - case 'editingProcessor': - processorsDispatch({ - type: 'updateProcessor', - payload: { - processor: { - ...editorMode.arg.processor, - ...processorTypeAndOptions, - }, - selector: editorMode.arg.selector, - }, - }); - break; - default: - } - setEditorMode({ id: 'idle' }); - }, - [processorsDispatch, editorMode, setEditorMode] - ); - - const onCloseSettingsForm = useCallback(() => { - setEditorMode({ id: 'idle' }); - setFormState({ validate: () => Promise.resolve(true) }); - }, [setFormState, setEditorMode]); - - const onTreeAction = useCallback( - (action) => { - switch (action.type) { - case 'addProcessor': - setEditorMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } }); - break; - case 'move': - setEditorMode({ id: 'idle' }); - processorsDispatch({ - type: 'moveProcessor', - payload: action.payload, - }); - break; - case 'selectToMove': - setEditorMode({ id: 'movingProcessor', arg: action.payload.info }); - break; - case 'cancelMove': - setEditorMode({ id: 'idle' }); - break; - } - }, - [processorsDispatch, setEditorMode] - ); - - const movingProcessor = editorMode.id === 'movingProcessor' ? editorMode.arg : undefined; - - return ( -
- - - - - - - - - - - - - - - - } - checked={showGlobalOnFailure} - onChange={(e) => setShowGlobalOnFailure(e.target.checked)} - data-test-subj="pipelineEditorOnFailureToggle" - /> - - {showGlobalOnFailure ? ( - - - - ) : undefined} - - {editorMode.id === 'editingProcessor' || editorMode.id === 'creatingProcessor' ? ( - - ) : undefined} - {editorMode.id === 'removingProcessor' && ( - { - if (confirmed) { - processorsDispatch({ - type: 'removeProcessor', - payload: { selector }, - }); - } - setEditorMode({ id: 'idle' }); - }} - /> - )} -
- ); - } -); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts index 7265f63f45a5d..0e06b8d55d379 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts @@ -12,6 +12,6 @@ export { Action, } from './processors_reducer'; -export { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './constants'; +export * from './constants'; export { isChildPath, isOnFailureSelector } from './utils'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts index 4e069aab8bdd1..295e7ff141117 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts @@ -38,6 +38,12 @@ export type Action = payload: { source: ProcessorSelector; }; + } + | { + type: 'loadProcessors'; + payload: { + newState: DeserializeResult; + }; }; export type ProcessorsDispatch = Dispatch; @@ -124,6 +130,14 @@ export const reducer: Reducer = (state, action) => { return setValue(sourceProcessorsArraySelector, state, sourceProcessorsArray); } + if (action.type === 'loadProcessors') { + return { + ...action.payload.newState, + onFailure: action.payload.newState.onFailure ?? [], + isRoot: true, + }; + } + return state; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts index aa39fca29fa8b..aea8f0f0910f4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts @@ -38,6 +38,8 @@ export interface OnUpdateHandlerArg extends FormValidityState { getData: () => SerializeResult; } +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + /** * The editor can be in different modes. This enables us to hold * a reference to data dispatch to the reducer (like the {@link ProcessorSelector} diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 05e7d1e41c5fa..d2c4b73a48767 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -22,6 +22,8 @@ export { UseRequestConfig, WithPrivileges, Monaco, + JsonEditor, + OnJsonEditorUpdateHandler, } from '../../../../src/plugins/es_ui_shared/public/'; export { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index d62f3dbcf029a..b41e93def966e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -55,7 +55,7 @@ export function getSavedObjectFormat({ state: { datasourceStates, datasourceMetaData: { - filterableIndexPatterns: _.uniq(filterableIndexPatterns, 'id'), + filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'), }, visualization: visualization.getPersistableState(state.visualization.state), query: framePublicAPI.query, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 851a9f4653fec..94c0f4083dfee 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 0e7cefb58fc28..eb7940634d78e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -5,7 +5,7 @@ */ import './datapanel.scss'; -import { uniq, indexBy, groupBy, throttle } from 'lodash'; +import { uniq, keyBy, groupBy, throttle } from 'lodash'; import React, { useState, useEffect, memo, useCallback, useMemo } from 'react'; import { EuiFlexGroup, @@ -256,7 +256,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ const fieldGroups: FieldsGroup = useMemo(() => { const containsData = (field: IndexPatternField) => { - const fieldByName = indexBy(allFields, 'name'); + const fieldByName = keyBy(allFields, 'name'); const overallField = fieldByName[field.name]; return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx index df49ed828a190..ebb706258cf45 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiHorizontalRule, EuiRadio, EuiSelect, htmlIdGenerator } from '@elastic/eui'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 34a4384ec0d40..5b84108b99dd9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -119,7 +119,7 @@ export function PopoverEditor(props: PopoverEditorProps) { validOperationTypes.push(...operationByField[selectedColumn.sourceField]!); } - return _.uniq( + return _.uniqBy( [ ...asOperationOptions(validOperationTypes, true), ...asOperationOptions(possibleOperationTypes, false), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 89e2c753f4c76..111a113a16be7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -57,14 +57,11 @@ function buildSuggestion({ // two match up. const layers = _.mapValues(updatedState.layers, (layer) => ({ ...layer, - columns: _.pick, Record>( - layer.columns, - layer.columnOrder - ), + columns: _.pick(layer.columns, layer.columnOrder) as Record, })); const columnOrder = layers[layerId].columnOrder; - const columnMap = layers[layerId].columns; + const columnMap = layers[layerId].columns as Record; const isMultiRow = Object.values(columnMap).some((column) => column.isBucketed); return { @@ -108,7 +105,10 @@ export function getDatasourceSuggestionsForField( // The field we're suggesting on matches an existing layer. In this case we find the layer with // the fewest configured columns and try to add the field to this table. If this layer does not // contain any layers yet, behave as if there is no layer. - const mostEmptyLayerId = _.min(layerIds, (layerId) => state.layers[layerId].columnOrder.length); + const mostEmptyLayerId = _.minBy( + layerIds, + (layerId) => state.layers[layerId].columnOrder.length + ) as string; if (state.layers[mostEmptyLayerId].columnOrder.length === 0) { return getEmptyLayerSuggestionsForField(state, mostEmptyLayerId, indexPatternId, field); } else { @@ -491,7 +491,7 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId } function getMetricColumn(indexPattern: IndexPattern, layerId: string, field: IndexPatternField) { - const operationDefinitionsMap = _.indexBy(operationDefinitions, 'type'); + const operationDefinitionsMap = _.keyBy(operationDefinitions, 'type'); const [column] = getOperationTypesForField(field) .map((type) => operationDefinitionsMap[type].buildColumn({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index eea00d52a77f9..1ae10e07b0c24 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { DatasourceLayerPanelProps } from '../types'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 101f536993365..e995c7317b5d8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -96,7 +96,7 @@ export async function loadInitialState({ const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); - const requiredPatterns = _.unique( + const requiredPatterns = _.uniq( state ? Object.values(state.layers) .map((l) => l.indexPatternId) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index a04f71a9095c5..9e5a0f496357d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { DimensionPriority, OperationMetadata } from '../../types'; import { operationDefinitionMap, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts index 3a1aaaa819dc0..51691ae18a99a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts @@ -161,7 +161,7 @@ export function updateLayerIndexPattern( layer: IndexPatternLayer, newIndexPattern: IndexPattern ): IndexPatternLayer { - const keptColumns: IndexPatternLayer['columns'] = _.pick(layer.columns, (column) => + const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => isColumnTransferable(column, newIndexPattern) ); const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index e507bee2a898d..9473a1523b8ca 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index fadee01e695d5..0cd92fd96c952 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { DraggedField } from './indexpattern'; import { BaseIndexPatternColumn, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index 474ea5c5b08cd..d38f51cb1621a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -31,7 +31,7 @@ function getVisualizationType(state: State): VisualizationType | 'mixed' { ); } const visualizationType = visualizationTypes.find((t) => t.id === state.layers[0].seriesType); - const seriesTypes = _.unique(state.layers.map((l) => l.seriesType)); + const seriesTypes = _.uniq(state.layers.map((l) => l.seriesType)); return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; } diff --git a/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js index 3a304e467e0c0..7d541ec192e0f 100755 --- a/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js +++ b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick, capitalize } from 'lodash'; +import { pick, upperFirst } from 'lodash'; import moment from 'moment'; import { getSearchValue } from '../../lib/get_search_value'; @@ -26,7 +26,7 @@ export class PipelineListItem { if (props.lastModified) { this.lastModified = getMomentDate(props.lastModified); - this.lastModifiedHumanized = capitalize(this.lastModified.fromNow()); + this.lastModifiedHumanized = upperFirst(this.lastModified.fromNow()); } } diff --git a/x-pack/plugins/logstash/server/models/cluster/cluster.ts b/x-pack/plugins/logstash/server/models/cluster/cluster.ts index 54f03605e14d6..6bc57eae41b76 100755 --- a/x-pack/plugins/logstash/server/models/cluster/cluster.ts +++ b/x-pack/plugins/logstash/server/models/cluster/cluster.ts @@ -25,7 +25,7 @@ export class Cluster { // generate Pipeline object from elasticsearch response static fromUpstreamJSON(upstreamCluster: Record) { - const uuid = get(upstreamCluster, 'cluster_uuid'); + const uuid = get(upstreamCluster, 'cluster_uuid') as string; return new Cluster({ uuid }); } } diff --git a/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts index 3f2debeebeb46..8ce04c83afdbf 100755 --- a/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts +++ b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts @@ -103,11 +103,11 @@ export class Pipeline { ) ); } - const id = get(upstreamPipeline, '_id'); - const description = get(upstreamPipeline, '_source.description'); - const username = get(upstreamPipeline, '_source.username'); - const pipeline = get(upstreamPipeline, '_source.pipeline'); - const settings = get>(upstreamPipeline, '_source.pipeline_settings'); + const id = get(upstreamPipeline, '_id') as string; + const description = get(upstreamPipeline, '_source.description') as string; + const username = get(upstreamPipeline, '_source.username') as string; + const pipeline = get(upstreamPipeline, '_source.pipeline') as string; + const settings = get(upstreamPipeline, '_source.pipeline_settings') as Record; const opts: PipelineOptions = { id, description, username, pipeline, settings }; diff --git a/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.ts b/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.ts index 98c91fca1fcca..eeb197a58f51d 100755 --- a/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.ts +++ b/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.ts @@ -37,9 +37,9 @@ export class PipelineListItem { static fromUpstreamJSON(pipeline: Hit) { const opts = { id: pipeline._id, - description: get(pipeline, '_source.description'), - last_modified: get(pipeline, '_source.last_modified'), - username: get(pipeline, '_source.username'), + description: get(pipeline, '_source.description') as string, + last_modified: get(pipeline, '_source.last_modified') as string, + username: get(pipeline, '_source.username') as string, }; return new PipelineListItem(opts); diff --git a/x-pack/plugins/maps/common/descriptor_types/sources.ts b/x-pack/plugins/maps/common/descriptor_types/sources.ts index 86ace0e32cc84..e32b5f44c8272 100644 --- a/x-pack/plugins/maps/common/descriptor_types/sources.ts +++ b/x-pack/plugins/maps/common/descriptor_types/sources.ts @@ -169,6 +169,7 @@ export type LayerDescriptor = { alpha?: number; id: string; label?: string | null; + areLabelsOnTop?: boolean; minZoom?: number; maxZoom?: number; sourceDescriptor: SourceDescriptor | null; diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 51e251a5d8e20..a0d2152e8866c 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -13,6 +13,7 @@ import { getLayerListRaw, getSelectedLayerId, getMapReady, + getMapColors, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; import { cancelRequest } from '../reducers/non_serializable_instances'; @@ -318,6 +319,15 @@ export function updateLayerAlpha(id: string, alpha: number) { }; } +export function updateLabelsOnTop(id: string, areLabelsOnTop: boolean) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'areLabelsOnTop', + newValue: areLabelsOnTop, + }; +} + export function setLayerQuery(id: string, query: Query) { return (dispatch: Dispatch) => { dispatch({ @@ -384,7 +394,8 @@ export function clearMissingStyleProperties(layerId: string) { const nextFields = await (targetLayer as IVectorLayer).getFields(); // take into account all fields, since labels can be driven by any field (source or join) const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved( - nextFields + nextFields, + getMapColors(getState()) ); if (hasChanges && nextStyleDescriptor) { dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index e122d1cda3ed9..d6f6ee8fa609b 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -76,6 +76,8 @@ export interface ILayer { getPrevRequestToken(dataId: string): symbol | undefined; destroy: () => void; isPreviewLayer: () => boolean; + areLabelsOnTop: () => boolean; + supportsLabelsOnTop: () => boolean; } export type Footnote = { icon: ReactElement; @@ -483,4 +485,12 @@ export class AbstractLayer implements ILayer { getType(): string | undefined { return this._descriptor.type; } + + areLabelsOnTop(): boolean { + return false; + } + + supportsLabelsOnTop(): boolean { + return false; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js index 61ec02e72adf2..96dad0c01139e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js @@ -277,4 +277,12 @@ export class VectorTileLayer extends TileLayer { this._setOpacityForType(mbMap, mbLayer, mbLayerId); }); } + + areLabelsOnTop() { + return !!this._descriptor.areLabelsOnTop; + } + + supportsLabelsOnTop() { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js index 97afac9ef1745..e20c509ccd4a2 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js @@ -10,6 +10,8 @@ import { esAggFieldsFactory } from '../../fields/es_agg_field'; import { AGG_TYPE, COUNT_PROP_LABEL, FIELD_ORIGIN } from '../../../../common/constants'; import { getSourceAggKey } from '../../../../common/get_agg_key'; +export const DEFAULT_METRIC = { type: AGG_TYPE.COUNT }; + export class AbstractESAggSource extends AbstractESSource { constructor(descriptor, inspectorAdapters) { super(descriptor, inspectorAdapters); @@ -48,6 +50,7 @@ export class AbstractESAggSource extends AbstractESSource { getMetricFields() { const metrics = this._metricFields.filter((esAggField) => esAggField.isValid()); + // Handle case where metrics is empty because older saved object state is empty array or there are no valid aggs. return metrics.length === 0 ? esAggFieldsFactory({ type: AGG_TYPE.COUNT }, this, this.getOriginForField()) : metrics; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js index b613f577067ba..9431fb55dc88b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js @@ -18,7 +18,7 @@ import { } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { AbstractESAggSource } from '../es_agg_source'; +import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source'; import { DataRequestAbortError } from '../../util/data_request'; import { registerSource } from '../source_registry'; import { makeESBbox } from '../../../elasticsearch_geo_utils'; @@ -42,7 +42,7 @@ export class ESGeoGridSource extends AbstractESAggSource { id: uuid(), indexPatternId, geoField, - metrics: metrics ? metrics : [], + metrics: metrics ? metrics : [DEFAULT_METRIC], requestType, resolution: resolution ? resolution : GRID_RESOLUTION.COARSE, }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 076e7a758a4fb..a4cff7c89a011 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; -import { AbstractESAggSource } from '../es_agg_source'; +import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source'; import { indexPatterns } from '../../../../../../../src/plugins/data/public'; import { registerSource } from '../source_registry'; @@ -32,7 +32,7 @@ export class ESPewPewSource extends AbstractESAggSource { indexPatternId: indexPatternId, sourceGeoField, destGeoField, - metrics: metrics ? metrics : [], + metrics: metrics ? metrics : [DEFAULT_METRIC], }; } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx index a959912718197..8c2f5e271ff5c 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/maps/public/classes/styles/style.ts b/x-pack/plugins/maps/public/classes/styles/style.ts index 7d39acd504c42..1859c7875ad11 100644 --- a/x-pack/plugins/maps/public/classes/styles/style.ts +++ b/x-pack/plugins/maps/public/classes/styles/style.ts @@ -13,7 +13,8 @@ import { DataRequest } from '../util/data_request'; export interface IStyle { getDescriptor(): StyleDescriptor | null; getDescriptorWithMissingStylePropsRemoved( - nextFields: IField[] + nextFields: IField[], + mapColors: string[] ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor }; pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor; renderEditor({ @@ -34,7 +35,8 @@ export class AbstractStyle implements IStyle { } getDescriptorWithMissingStylePropsRemoved( - nextFields: IField[] + nextFields: IField[], + mapColors: string[] ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor } { return { hasChanges: false, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx index 550b3737963d5..fac002d0a877d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx @@ -5,7 +5,6 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import _ from 'lodash'; import React from 'react'; import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js index 04a5381fa2592..3cff48e4d682e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js @@ -7,7 +7,12 @@ import _ from 'lodash'; import React from 'react'; import { VectorStyleEditor } from './components/vector_style_editor'; -import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES } from './vector_style_defaults'; +import { + getDefaultProperties, + getDefaultStaticProperties, + LINE_STYLES, + POLYGON_STYLES, +} from './vector_style_defaults'; import { AbstractStyle } from '../style'; import { GEO_JSON_TYPE, @@ -191,7 +196,7 @@ export class VectorStyle extends AbstractStyle { * This method does not update its descriptor. It just returns a new descriptor that the caller * can then use to update store state via dispatch. */ - getDescriptorWithMissingStylePropsRemoved(nextFields) { + getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors) { const originalProperties = this.getRawProperties(); const updatedProperties = {}; @@ -201,6 +206,13 @@ export class VectorStyle extends AbstractStyle { }); dynamicProperties.forEach((key) => { + // Convert dynamic styling to static stying when there are no nextFields + if (nextFields.length === 0) { + const staticProperties = getDefaultStaticProperties(mapColors); + updatedProperties[key] = staticProperties[key]; + return; + } + const dynamicProperty = originalProperties[key]; const fieldName = dynamicProperty && dynamicProperty.options.field && dynamicProperty.options.field.name; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index a0dc07b8e545b..a85cd0cc86407 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -6,7 +6,12 @@ import { VectorStyle } from './vector_style'; import { DataRequest } from '../../util/data_request'; -import { FIELD_ORIGIN, STYLE_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { + FIELD_ORIGIN, + STYLE_TYPE, + VECTOR_SHAPE_TYPE, + VECTOR_STYLES, +} from '../../../../common/constants'; jest.mock('../../../kibana_services'); jest.mock('ui/new_platform'); @@ -42,6 +47,7 @@ class MockSource { describe('getDescriptorWithMissingStylePropsRemoved', () => { const fieldName = 'doIStillExist'; + const mapColors = []; const properties = { fillColor: { type: STYLE_TYPE.STATIC, @@ -59,7 +65,8 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { iconSize: { type: STYLE_TYPE.DYNAMIC, options: { - color: 'a color', + minSize: 1, + maxSize: 10, field: { name: fieldName, origin: FIELD_ORIGIN.SOURCE }, }, }, @@ -75,86 +82,55 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { const vectorStyle = new VectorStyle({ properties }, new MockSource()); const nextFields = [new MockField({ fieldName })]; - const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields); + const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved( + nextFields, + mapColors + ); expect(hasChanges).toBe(false); }); it('Should clear missing fields when next ordinal fields do not contain existing style property fields', () => { const vectorStyle = new VectorStyle({ properties }, new MockSource()); - const nextFields = []; + const nextFields = [new MockField({ fieldName: 'someOtherField' })]; const { hasChanges, nextStyleDescriptor, - } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields); + } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors); expect(hasChanges).toBe(true); - expect(nextStyleDescriptor.properties).toEqual({ - fillColor: { - options: {}, - type: 'STATIC', - }, - icon: { - options: { - value: 'marker', - }, - type: 'STATIC', - }, - iconOrientation: { - options: { - orientation: 0, - }, - type: 'STATIC', - }, - iconSize: { - options: { - color: 'a color', - }, - type: 'DYNAMIC', - }, - labelText: { - options: { - value: '', - }, - type: 'STATIC', - }, - labelBorderColor: { - options: { - color: '#FFFFFF', - }, - type: 'STATIC', - }, - labelBorderSize: { - options: { - size: 'SMALL', - }, - }, - labelColor: { - options: { - color: '#000000', - }, - type: 'STATIC', - }, - labelSize: { - options: { - size: 14, - }, - type: 'STATIC', - }, - lineColor: { - options: {}, - type: 'DYNAMIC', + expect(nextStyleDescriptor.properties[VECTOR_STYLES.LINE_COLOR]).toEqual({ + options: {}, + type: 'DYNAMIC', + }); + expect(nextStyleDescriptor.properties[VECTOR_STYLES.ICON_SIZE]).toEqual({ + options: { + minSize: 1, + maxSize: 10, }, - lineWidth: { - options: { - size: 1, - }, - type: 'STATIC', + type: 'DYNAMIC', + }); + }); + + it('Should convert dynamic styles to static styles when there are no next fields', () => { + const vectorStyle = new VectorStyle({ properties }, new MockSource()); + + const nextFields = []; + const { + hasChanges, + nextStyleDescriptor, + } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors); + expect(hasChanges).toBe(true); + expect(nextStyleDescriptor.properties[VECTOR_STYLES.LINE_COLOR]).toEqual({ + options: { + color: '#41937c', }, - symbolizeAs: { - options: { - value: 'circle', - }, + type: 'STATIC', + }); + expect(nextStyleDescriptor.properties[VECTOR_STYLES.ICON_SIZE]).toEqual({ + options: { + size: 6, }, + type: 'STATIC', }); }); }); diff --git a/x-pack/plugins/maps/public/classes/util/data_request.ts b/x-pack/plugins/maps/public/classes/util/data_request.ts index 44b7b2ffb6ae7..42c19b8c641e0 100644 --- a/x-pack/plugins/maps/public/classes/util/data_request.ts +++ b/x-pack/plugins/maps/public/classes/util/data_request.ts @@ -5,7 +5,6 @@ */ /* eslint-disable max-classes-per-file */ -import _ from 'lodash'; import { DataRequestDescriptor, DataMeta } from '../../../common/descriptor_types'; export class DataRequest { diff --git a/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap new file mode 100644 index 0000000000000..0d4f1f99e464c --- /dev/null +++ b/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should add default count metric when metrics is empty array 1`] = ` + +
+
+ +
+
+ + + + + + +
+`; + +exports[`should render metrics editor 1`] = ` + +
+
+ +
+
+ + + + + + +
+`; diff --git a/x-pack/plugins/maps/public/components/metrics_editor.js b/x-pack/plugins/maps/public/components/metrics_editor.js index 6c5a9af8f0f02..7d4d7bf3ec7ab 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor.js +++ b/x-pack/plugins/maps/public/components/metrics_editor.js @@ -10,11 +10,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiSpacer, EuiTextAlign } from '@elastic/eui'; import { MetricEditor } from './metric_editor'; -import { AGG_TYPE } from '../../common/constants'; +import { DEFAULT_METRIC } from '../classes/sources/es_agg_source'; export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, metricsFilter }) { function renderMetrics() { - return metrics.map((metric, index) => { + // There was a bug in 7.8 that initialized metrics to []. + // This check is needed to handle any saved objects created before the bug was patched. + const nonEmptyMetrics = metrics.length === 0 ? [DEFAULT_METRIC] : metrics; + return nonEmptyMetrics.map((metric, index) => { const onMetricChange = (metric) => { onChange([...metrics.slice(0, index), metric, ...metrics.slice(index + 1)]); }; @@ -100,6 +103,6 @@ MetricsEditor.propTypes = { }; MetricsEditor.defaultProps = { - metrics: [{ type: AGG_TYPE.COUNT }], + metrics: [DEFAULT_METRIC], allowMultipleMetrics: true, }; diff --git a/x-pack/plugins/maps/public/components/metrics_editor.test.js b/x-pack/plugins/maps/public/components/metrics_editor.test.js new file mode 100644 index 0000000000000..bcbeef29875ee --- /dev/null +++ b/x-pack/plugins/maps/public/components/metrics_editor.test.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { MetricsEditor } from './metrics_editor'; +import { AGG_TYPE } from '../../common/constants'; + +const defaultProps = { + metrics: [ + { + type: AGG_TYPE.SUM, + field: 'myField', + }, + ], + fields: [], + onChange: () => {}, + allowMultipleMetrics: true, + metricsFilter: () => {}, +}; + +test('should render metrics editor', async () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should add default count metric when metrics is empty array', async () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap index 1620e3058be67..1c48ed2290dce 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap @@ -89,7 +89,19 @@ exports[`LayerPanel is rendered 1`] = ` className="mapLayerPanel__bodyOverflow" > - +
mockSourceSettings
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js deleted file mode 100644 index 0d2732184afc4..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { LayerSettings } from './layer_settings'; -import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { - updateLayerLabel, - updateLayerMaxZoom, - updateLayerMinZoom, - updateLayerAlpha, -} from '../../../actions'; -import { MAX_ZOOM } from '../../../../common/constants'; - -function mapStateToProps(state = {}) { - const selectedLayer = getSelectedLayer(state); - return { - minVisibilityZoom: selectedLayer.getMinSourceZoom(), - maxVisibilityZoom: MAX_ZOOM, - alpha: selectedLayer.getAlpha(), - label: selectedLayer.getLabel(), - layerId: selectedLayer.getId(), - maxZoom: selectedLayer.getMaxZoom(), - minZoom: selectedLayer.getMinZoom(), - }; -} - -function mapDispatchToProps(dispatch) { - return { - updateLabel: (id, label) => dispatch(updateLayerLabel(id, label)), - updateMinZoom: (id, minZoom) => dispatch(updateLayerMinZoom(id, minZoom)), - updateMaxZoom: (id, maxZoom) => dispatch(updateLayerMaxZoom(id, maxZoom)), - updateAlpha: (id, alpha) => dispatch(updateLayerAlpha(id, alpha)), - }; -} - -const connectedLayerSettings = connect(mapStateToProps, mapDispatchToProps)(LayerSettings); -export { connectedLayerSettings as LayerSettings }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.tsx new file mode 100644 index 0000000000000..d2468496fbe0d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { LayerSettings } from './layer_settings'; +import { + updateLayerLabel, + updateLayerMaxZoom, + updateLayerMinZoom, + updateLayerAlpha, + updateLabelsOnTop, +} from '../../../actions'; + +function mapDispatchToProps(dispatch: Dispatch) { + return { + updateLabel: (id: string, label: string) => dispatch(updateLayerLabel(id, label)), + updateMinZoom: (id: string, minZoom: number) => dispatch(updateLayerMinZoom(id, minZoom)), + updateMaxZoom: (id: string, maxZoom: number) => dispatch(updateLayerMaxZoom(id, maxZoom)), + updateAlpha: (id: string, alpha: number) => dispatch(updateLayerAlpha(id, alpha)), + updateLabelsOnTop: (id: string, areLabelsOnTop: boolean) => + dispatch(updateLabelsOnTop(id, areLabelsOnTop)), + }; +} + +const connectedLayerSettings = connect(null, mapDispatchToProps)(LayerSettings); +export { connectedLayerSettings as LayerSettings }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js deleted file mode 100644 index bc99285cfc7aa..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ /dev/null @@ -1,87 +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, { Fragment } from 'react'; - -import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; - -import { AlphaSlider } from '../../../components/alpha_slider'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; -export function LayerSettings(props) { - const onLabelChange = (event) => { - const label = event.target.value; - props.updateLabel(props.layerId, label); - }; - - const onZoomChange = ([min, max]) => { - props.updateMinZoom(props.layerId, Math.max(props.minVisibilityZoom, parseInt(min, 10))); - props.updateMaxZoom(props.layerId, Math.min(props.maxVisibilityZoom, parseInt(max, 10))); - }; - - const onAlphaChange = (alpha) => { - props.updateAlpha(props.layerId, alpha); - }; - - const renderZoomSliders = () => { - return ( - - ); - }; - - const renderLabel = () => { - return ( - - - - ); - }; - - return ( - - - -
- -
-
- - - {renderLabel()} - {renderZoomSliders()} - -
- - -
- ); -} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx new file mode 100644 index 0000000000000..33d684b320208 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx @@ -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 React, { ChangeEvent, Fragment } from 'react'; +import { + EuiTitle, + EuiPanel, + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiSwitch, + EuiSwitchEvent, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MAX_ZOOM } from '../../../../common/constants'; +import { AlphaSlider } from '../../../components/alpha_slider'; +import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; +import { ILayer } from '../../../classes/layers/layer'; + +interface Props { + layer: ILayer; + updateLabel: (layerId: string, label: string) => void; + updateMinZoom: (layerId: string, minZoom: number) => void; + updateMaxZoom: (layerId: string, maxZoom: number) => void; + updateAlpha: (layerId: string, alpha: number) => void; + updateLabelsOnTop: (layerId: string, areLabelsOnTop: boolean) => void; +} + +export function LayerSettings(props: Props) { + const minVisibilityZoom = props.layer.getMinSourceZoom(); + const maxVisibilityZoom = MAX_ZOOM; + const layerId = props.layer.getId(); + + const onLabelChange = (event: ChangeEvent) => { + const label = event.target.value; + props.updateLabel(layerId, label); + }; + + const onZoomChange = (value: [string, string]) => { + props.updateMinZoom(layerId, Math.max(minVisibilityZoom, parseInt(value[0], 10))); + props.updateMaxZoom(layerId, Math.min(maxVisibilityZoom, parseInt(value[1], 10))); + }; + + const onAlphaChange = (alpha: number) => { + props.updateAlpha(layerId, alpha); + }; + + const onLabelsOnTopChange = (event: EuiSwitchEvent) => { + props.updateLabelsOnTop(layerId, event.target.checked); + }; + + const renderZoomSliders = () => { + return ( + + ); + }; + + const renderLabel = () => { + return ( + + + + ); + }; + + const renderShowLabelsOnTop = () => { + if (!props.layer.supportsLabelsOnTop()) { + return null; + } + + return ( + + + + ); + }; + + return ( + + + +
+ +
+
+ + + {renderLabel()} + {renderZoomSliders()} + + {renderShowLabelsOnTop()} +
+ + +
+ ); +} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js index 557fe5fd5f705..71d76ff53d8a9 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js @@ -205,7 +205,7 @@ export class LayerPanel extends React.Component {
- + {this.props.selectedLayer.renderSourceSettingsEditor({ onChange: this._onSourceChange, diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js index 376010f0df9ba..e2050724ef684 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { removeOrphanedSourcesAndLayers, syncLayerOrderForSingleLayer } from './utils'; +import { removeOrphanedSourcesAndLayers } from './utils'; import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; import _ from 'lodash'; @@ -186,80 +186,3 @@ describe('removeOrphanedSourcesAndLayers', () => { expect(mockMbMap.getStyle()).toEqual(styleWithSpatialFilters); }); }); - -describe('syncLayerOrderForSingleLayer', () => { - test('should move bar layer in front of foo layer', async () => { - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeSingleSourceMockLayer('bar'); - - const currentLayerOrder = [fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); - - test('should fail at moving multiple layers (this tests a limitation of the sync)', async () => { - //This is a known limitation of the layer order syncing. - //It assumes only a single layer will have moved. - //In practice, the Maps app will likely not cause multiple layers to move at once: - // - the UX only allows dragging a single layer - // - redux triggers a updates frequently enough - //But this is conceptually "wrong", as the sync does not actually operate in the same way as all the other mb-syncing methods - - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeSingleSourceMockLayer('bar'); - const foozLayer = makeSingleSourceMockLayer('foo'); - const bazLayer = makeSingleSourceMockLayer('baz'); - - const currentLayerOrder = [fooLayer, barLayer, foozLayer, bazLayer]; - const nextLayerListOrder = [bazLayer, barLayer, foozLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - const isSyncSuccesful = _.isEqual(orderedStyle, nextStyle); - expect(isSyncSuccesful).toEqual(false); - }); - - test('should move bar layer in front of foo layer (multi source)', async () => { - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeMultiSourceMockLayer('bar'); - - const currentLayerOrder = [fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); - - test('should move bar layer in front of foo layer, but after baz layer', async () => { - const bazLayer = makeSingleSourceMockLayer('baz'); - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeSingleSourceMockLayer('bar'); - - const currentLayerOrder = [bazLayer, fooLayer, barLayer]; - const nextLayerListOrder = [bazLayer, barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); -}); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts new file mode 100644 index 0000000000000..273611e94ee40 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable max-classes-per-file */ + +import _ from 'lodash'; +import { Map as MbMap, Layer as MbLayer, Style as MbStyle } from 'mapbox-gl'; +import { getIsTextLayer, syncLayerOrder } from './sort_layers'; +import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; +import { ILayer } from '../../../classes/layers/layer'; + +let moveCounter = 0; + +class MockMbMap { + private _style: MbStyle; + + constructor(style: MbStyle) { + this._style = _.cloneDeep(style); + } + + getStyle() { + return _.cloneDeep(this._style); + } + + moveLayer(id: string, beforeId?: string) { + moveCounter++; + + if (!this._style.layers) { + throw new Error(`Can not move layer, mapbox style does not contain layers`); + } + + const layerIndex = this._style.layers.findIndex((layer) => { + return layer.id === id; + }); + if (layerIndex === -1) { + throw new Error(`Can not move layer, layer with id: ${id} does not exist`); + } + const moveMbLayer = this._style.layers[layerIndex]; + + if (beforeId) { + const beforeLayerIndex = this._style.layers.findIndex((mbLayer) => { + return mbLayer.id === beforeId; + }); + if (beforeLayerIndex === -1) { + throw new Error(`Can not move layer, before layer with id: ${id} does not exist`); + } + this._style.layers.splice(beforeLayerIndex, 0, moveMbLayer); + } else { + const topIndex = this._style.layers.length; + this._style.layers.splice(topIndex, 0, moveMbLayer); + } + + // Remove layer from previous location + this._style.layers.splice(layerIndex, 1); + + return this; + } +} + +class MockMapLayer { + private readonly _id: string; + private readonly _areLabelsOnTop: boolean; + + constructor(id: string, areLabelsOnTop: boolean) { + this._id = id; + this._areLabelsOnTop = areLabelsOnTop; + } + + ownsMbLayerId(mbLayerId: string) { + return mbLayerId.startsWith(this._id); + } + + areLabelsOnTop() { + return this._areLabelsOnTop; + } + + getId() { + return this._id; + } +} + +test('getIsTextLayer', () => { + const paintLabelMbLayer = { + id: `mylayer_text`, + type: 'symbol', + paint: { 'text-color': 'red' }, + } as MbLayer; + expect(getIsTextLayer(paintLabelMbLayer)).toBe(true); + + const layoutLabelMbLayer = { + id: `mylayer_text`, + type: 'symbol', + layout: { 'text-size': 'red' }, + } as MbLayer; + expect(getIsTextLayer(layoutLabelMbLayer)).toBe(true); + + const iconMbLayer = { + id: `mylayer_text`, + type: 'symbol', + paint: { 'icon-color': 'house' }, + } as MbLayer; + expect(getIsTextLayer(iconMbLayer)).toBe(false); + + const circleMbLayer = { id: `mylayer_text`, type: 'circle' } as MbLayer; + expect(getIsTextLayer(circleMbLayer)).toBe(false); +}); + +describe('sortLayer', () => { + const ALPHA_LAYER_ID = 'alpha'; + const BRAVO_LAYER_ID = 'bravo'; + const CHARLIE_LAYER_ID = 'charlie'; + + const spatialFilterLayer = (new MockMapLayer( + SPATIAL_FILTERS_LAYER_ID, + false + ) as unknown) as ILayer; + const mapLayers = [ + (new MockMapLayer(CHARLIE_LAYER_ID, true) as unknown) as ILayer, + (new MockMapLayer(BRAVO_LAYER_ID, false) as unknown) as ILayer, + (new MockMapLayer(ALPHA_LAYER_ID, false) as unknown) as ILayer, + ]; + + beforeEach(() => { + moveCounter = 0; + }); + + // Initial order that styles are added to mapbox is non-deterministic and depends on the order of data fetches. + test('Should sort initial layer load order to expected order', () => { + const initialMbStyle = { + version: 0, + layers: [ + { id: `${BRAVO_LAYER_ID}_text`, type: 'symbol' } as MbLayer, + { id: `${BRAVO_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + { id: `${SPATIAL_FILTERS_LAYER_ID}_fill`, type: 'fill' } as MbLayer, + { id: `${SPATIAL_FILTERS_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + { + id: `${CHARLIE_LAYER_ID}_text`, + type: 'symbol', + paint: { 'text-color': 'red' }, + } as MbLayer, + { id: `${CHARLIE_LAYER_ID}_fill`, type: 'fill' } as MbLayer, + { id: `${ALPHA_LAYER_ID}_text`, type: 'symbol' } as MbLayer, + { id: `${ALPHA_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + ], + }; + const mbMap = new MockMbMap(initialMbStyle); + syncLayerOrder((mbMap as unknown) as MbMap, spatialFilterLayer, mapLayers); + const sortedMbStyle = mbMap.getStyle(); + const sortedMbLayerIds = sortedMbStyle.layers!.map((mbLayer) => { + return mbLayer.id; + }); + expect(sortedMbLayerIds).toEqual([ + 'charlie_fill', + 'bravo_text', + 'bravo_circle', + 'alpha_text', + 'alpha_circle', + 'charlie_text', + 'SPATIAL_FILTERS_LAYER_ID_fill', + 'SPATIAL_FILTERS_LAYER_ID_circle', + ]); + }); + + // Test case testing when layer is moved in Table of Contents + test('Should sort single layer single move to expected order', () => { + const initialMbStyle = { + version: 0, + layers: [ + { id: `${CHARLIE_LAYER_ID}_fill`, type: 'fill' } as MbLayer, + { id: `${ALPHA_LAYER_ID}_text`, type: 'symbol' } as MbLayer, + { id: `${ALPHA_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + { id: `${BRAVO_LAYER_ID}_text`, type: 'symbol' } as MbLayer, + { id: `${BRAVO_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + { + id: `${CHARLIE_LAYER_ID}_text`, + type: 'symbol', + paint: { 'text-color': 'red' }, + } as MbLayer, + { id: `${SPATIAL_FILTERS_LAYER_ID}_fill`, type: 'fill' } as MbLayer, + { id: `${SPATIAL_FILTERS_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + ], + }; + const mbMap = new MockMbMap(initialMbStyle); + syncLayerOrder((mbMap as unknown) as MbMap, spatialFilterLayer, mapLayers); + const sortedMbStyle = mbMap.getStyle(); + const sortedMbLayerIds = sortedMbStyle.layers!.map((mbLayer) => { + return mbLayer.id; + }); + expect(sortedMbLayerIds).toEqual([ + 'charlie_fill', + 'bravo_text', + 'bravo_circle', + 'alpha_text', + 'alpha_circle', + 'charlie_text', + 'SPATIAL_FILTERS_LAYER_ID_fill', + 'SPATIAL_FILTERS_LAYER_ID_circle', + ]); + }); + + test('Should not call move layers when layers are in expected order', () => { + const initialMbStyle = { + version: 0, + layers: [ + { id: `${CHARLIE_LAYER_ID}_fill`, type: 'fill' } as MbLayer, + { id: `${BRAVO_LAYER_ID}_text`, type: 'symbol' } as MbLayer, + { id: `${BRAVO_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + { id: `${ALPHA_LAYER_ID}_text`, type: 'symbol' } as MbLayer, + { id: `${ALPHA_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + { + id: `${CHARLIE_LAYER_ID}_text`, + type: 'symbol', + paint: { 'text-color': 'red' }, + } as MbLayer, + { id: `${SPATIAL_FILTERS_LAYER_ID}_fill`, type: 'fill' } as MbLayer, + { id: `${SPATIAL_FILTERS_LAYER_ID}_circle`, type: 'circle' } as MbLayer, + ], + }; + const mbMap = new MockMbMap(initialMbStyle); + syncLayerOrder((mbMap as unknown) as MbMap, spatialFilterLayer, mapLayers); + expect(moveCounter).toBe(0); + }); +}); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts new file mode 100644 index 0000000000000..4752eeba2376a --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Map as MbMap, Layer as MbLayer } from 'mapbox-gl'; +import { ILayer } from '../../../classes/layers/layer'; + +// "Layer" is overloaded and can mean the following +// 1) Map layer (ILayer): A single map layer consists of one to many mapbox layers. +// 2) Mapbox layer (MbLayer): Individual unit of rendering such as text, circles, polygons, or lines. + +export function getIsTextLayer(mbLayer: MbLayer) { + if (mbLayer.type !== 'symbol') { + return false; + } + + const styleNames = []; + if (mbLayer.paint) { + styleNames.push(...Object.keys(mbLayer.paint)); + } + if (mbLayer.layout) { + styleNames.push(...Object.keys(mbLayer.layout)); + } + return styleNames.some((styleName) => { + return styleName.startsWith('text-'); + }); +} + +function doesMbLayerBelongToMapLayerAndClass( + mapLayer: ILayer, + mbLayer: MbLayer, + layerClass: LAYER_CLASS +) { + if (!mapLayer.ownsMbLayerId(mbLayer.id)) { + return false; + } + + // mb layer belongs to mapLayer, now filter by layer class + if (layerClass === LAYER_CLASS.ANY) { + return true; + } + const isTextLayer = getIsTextLayer(mbLayer); + return layerClass === LAYER_CLASS.LABEL ? isTextLayer : !isTextLayer; +} + +enum LAYER_CLASS { + ANY = 'ANY', + LABEL = 'LABEL', + NON_LABEL = 'NON_LABEL', +} + +function moveMapLayer( + mbMap: MbMap, + mbLayers: MbLayer[], + mapLayer: ILayer, + layerClass: LAYER_CLASS, + beneathMbLayerId?: string +) { + mbLayers + .filter((mbLayer) => { + return doesMbLayerBelongToMapLayerAndClass(mapLayer, mbLayer, layerClass); + }) + .forEach((mbLayer) => { + mbMap.moveLayer(mbLayer.id, beneathMbLayerId); + }); +} + +function getBottomMbLayerId(mbLayers: MbLayer[], mapLayer: ILayer, layerClass: LAYER_CLASS) { + const bottomMbLayer = mbLayers.find((mbLayer) => { + return doesMbLayerBelongToMapLayerAndClass(mapLayer, mbLayer, layerClass); + }); + return bottomMbLayer ? bottomMbLayer.id : undefined; +} + +function isLayerInOrder( + mbMap: MbMap, + mapLayer: ILayer, + layerClass: LAYER_CLASS, + beneathMbLayerId?: string +) { + const mbLayers = mbMap.getStyle().layers!; // check ordering against mapbox to account for any upstream moves. + + if (!beneathMbLayerId) { + // Check that map layer is top layer + return doesMbLayerBelongToMapLayerAndClass(mapLayer, mbLayers[mbLayers.length - 1], layerClass); + } + + let inMapLayerBlock = false; + let nextMbLayerId = null; + for (let i = 0; i < mbLayers.length; i++) { + if (!inMapLayerBlock) { + if (doesMbLayerBelongToMapLayerAndClass(mapLayer, mbLayers[i], layerClass)) { + inMapLayerBlock = true; + } + } else { + // Next mbLayer not belonging to this map layer is the bottom mb layer for the next map layer + if (!doesMbLayerBelongToMapLayerAndClass(mapLayer, mbLayers[i], layerClass)) { + nextMbLayerId = mbLayers[i].id; + break; + } + } + } + + return nextMbLayerId === beneathMbLayerId; +} + +export function syncLayerOrder(mbMap: MbMap, spatialFiltersLayer: ILayer, layerList: ILayer[]) { + const mbLayers = mbMap.getStyle().layers; + if (!mbLayers || mbLayers.length === 0) { + return; + } + + // Ensure spatial filters layer is the top layer. + if (!isLayerInOrder(mbMap, spatialFiltersLayer, LAYER_CLASS.ANY)) { + moveMapLayer(mbMap, mbLayers, spatialFiltersLayer, LAYER_CLASS.ANY); + } + let beneathMbLayerId = getBottomMbLayerId(mbLayers, spatialFiltersLayer, LAYER_CLASS.ANY); + + // Sort map layer labels + [...layerList] + .reverse() + .filter((mapLayer) => { + return mapLayer.areLabelsOnTop(); + }) + .forEach((mapLayer: ILayer) => { + if (!isLayerInOrder(mbMap, mapLayer, LAYER_CLASS.LABEL, beneathMbLayerId)) { + moveMapLayer(mbMap, mbLayers, mapLayer, LAYER_CLASS.LABEL, beneathMbLayerId); + } + beneathMbLayerId = getBottomMbLayerId(mbLayers, mapLayer, LAYER_CLASS.LABEL); + }); + + // Sort map layers + [...layerList].reverse().forEach((mapLayer: ILayer) => { + const layerClass = mapLayer.areLabelsOnTop() ? LAYER_CLASS.NON_LABEL : LAYER_CLASS.ANY; + if (!isLayerInOrder(mbMap, mapLayer, layerClass, beneathMbLayerId)) { + moveMapLayer(mbMap, mbLayers, mapLayer, layerClass, beneathMbLayerId); + } + beneathMbLayerId = getBottomMbLayerId(mbLayers, mapLayer, layerClass); + }); +} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js index a5934038f83df..e5801afd5b601 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { RGBAImage } from './image_utils'; export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLayer) { @@ -45,84 +44,6 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLa mbSourcesToRemove.forEach((mbSourceId) => mbMap.removeSource(mbSourceId)); } -export function moveLayerToTop(mbMap, layer) { - const mbStyle = mbMap.getStyle(); - - if (!mbStyle.layers || mbStyle.layers.length === 0) { - return; - } - - layer.getMbLayerIds().forEach((mbLayerId) => { - const mbLayer = mbMap.getLayer(mbLayerId); - if (mbLayer) { - mbMap.moveLayer(mbLayerId); - } - }); -} - -/** - * This is function assumes only a single layer moved in the layerList, compared to mbMap - * It is optimized to minimize the amount of mbMap.moveLayer calls. - * @param mbMap - * @param layerList - */ -export function syncLayerOrderForSingleLayer(mbMap, layerList) { - if (!layerList || layerList.length === 0) { - return; - } - - const mbLayers = mbMap.getStyle().layers.slice(); - const layerIds = []; - mbLayers.forEach((mbLayer) => { - const layer = layerList.find((layer) => layer.ownsMbLayerId(mbLayer.id)); - if (layer) { - layerIds.push(layer.getId()); - } - }); - - const currentLayerOrderLayerIds = _.uniq(layerIds); - - const newLayerOrderLayerIdsUnfiltered = layerList.map((l) => l.getId()); - const newLayerOrderLayerIds = newLayerOrderLayerIdsUnfiltered.filter((layerId) => - currentLayerOrderLayerIds.includes(layerId) - ); - - let netPos = 0; - let netNeg = 0; - const movementArr = currentLayerOrderLayerIds.reduce((accu, id, idx) => { - const movement = newLayerOrderLayerIds.findIndex((newOId) => newOId === id) - idx; - movement > 0 ? netPos++ : movement < 0 && netNeg++; - accu.push({ id, movement }); - return accu; - }, []); - if (netPos === 0 && netNeg === 0) { - return; - } - const movedLayerId = - (netPos >= netNeg && movementArr.find((l) => l.movement < 0).id) || - (netPos < netNeg && movementArr.find((l) => l.movement > 0).id); - const nextLayerIdx = newLayerOrderLayerIds.findIndex((layerId) => layerId === movedLayerId) + 1; - - let nextMbLayerId; - if (nextLayerIdx === newLayerOrderLayerIds.length) { - nextMbLayerId = null; - } else { - const foundLayer = mbLayers.find(({ id: mbLayerId }) => { - const layerId = newLayerOrderLayerIds[nextLayerIdx]; - const layer = layerList.find((layer) => layer.getId() === layerId); - return layer.ownsMbLayerId(mbLayerId); - }); - nextMbLayerId = foundLayer.id; - } - - const movedLayer = layerList.find((layer) => layer.getId() === movedLayerId); - mbLayers.forEach(({ id: mbLayerId }) => { - if (movedLayer.ownsMbLayerId(mbLayerId)) { - mbMap.moveLayer(mbLayerId, nextMbLayerId); - } - }); -} - export async function addSpritesheetToMap(json, imgUrl, mbMap) { const imgData = await loadSpriteSheetImageData(imgUrl); addSpriteSheetToMapFromImageData(json, imgData, mbMap); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 42235bfd5442e..d96deb226744b 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -7,12 +7,8 @@ import _ from 'lodash'; import React from 'react'; import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; -import { - syncLayerOrderForSingleLayer, - removeOrphanedSourcesAndLayers, - addSpritesheetToMap, - moveLayerToTop, -} from './utils'; +import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils'; +import { syncLayerOrder } from './sort_layers'; import { getGlyphUrl, isRetina } from '../../../meta'; import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; @@ -265,8 +261,7 @@ export class MBMapContainer extends React.Component { this.props.spatialFiltersLayer ); this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap)); - syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList); - moveLayerToTop(this.state.mbMap, this.props.spatialFiltersLayer); + syncLayerOrder(this.state.mbMap, this.props.spatialFiltersLayer, this.props.layerList); }; _syncMbMapWithInspector = () => { diff --git a/x-pack/plugins/maps/public/index.scss b/x-pack/plugins/maps/public/index.scss index fe974fa610c03..d2dd07b0f81f9 100644 --- a/x-pack/plugins/maps/public/index.scss +++ b/x-pack/plugins/maps/public/index.scss @@ -1,8 +1,5 @@ /* GIS plugin styles */ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - // Prefix all styles with "map" to avoid conflicts. // Examples // mapChart diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 0e29eca244642..5f57d666b9f74 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -45,8 +45,8 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num ); const typeCountsSum = _.sum(typeCounts); accu[type] = { - min: typeCounts.length ? _.min(typeCounts) : 0, - max: typeCounts.length ? _.max(typeCounts) : 0, + min: typeCounts.length ? (_.min(typeCounts) as number) : 0, + max: typeCounts.length ? (_.max(typeCounts) as number) : 0, avg: typeCountsSum ? typeCountsSum / mapsCount : 0, }; return accu; @@ -115,9 +115,9 @@ export function buildMapsTelemetry({ const isEmsFile = _.get(layer, 'sourceDescriptor.type') === SOURCE_TYPES.EMS_FILE; return isEmsFile && _.get(layer, 'sourceDescriptor.id'); }) - .pick((val, key) => key !== 'false') + .pickBy((val, key) => key !== 'false') .value() - ); + ) as ILayerTypeCount[]; const dataSourcesCountSum = _.sum(dataSourcesCount); const layersCountSum = _.sum(layersCount); @@ -174,10 +174,10 @@ export async function getMapsTelemetry(config: MapsConfigType) { const savedObjectsClient = getInternalRepository(); // @ts-ignore const mapSavedObjects: MapSavedObject[] = await getMapSavedObjects(savedObjectsClient); - const indexPatternSavedObjects: IIndexPattern[] = await getIndexPatternSavedObjects( + const indexPatternSavedObjects: IIndexPattern[] = (await getIndexPatternSavedObjects( // @ts-ignore savedObjectsClient - ); + )) as IIndexPattern[]; const settings: SavedObjectAttribute = { showMapVisualizationTypes: config.showMapVisualizationTypes, }; diff --git a/x-pack/plugins/ml/public/application/_index.scss b/x-pack/plugins/ml/public/application/_index.scss index 11dc593a235a1..65e914a1ac923 100644 --- a/x-pack/plugins/ml/public/application/_index.scss +++ b/x-pack/plugins/ml/public/application/_index.scss @@ -1,6 +1,3 @@ -// Should import both the EUI constants and any Kibana ones that are considered global -@import 'src/legacy/ui/public/styles/styling_constants'; - // ML has it's own variables for coloring @import 'variables'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index a423722d1447a..5715687402bcb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -128,7 +128,7 @@ export interface Eval { export interface RegressionEvaluateResponse { regression: { - mean_squared_error: { + mse: { value: number; }; r_squared: { @@ -311,7 +311,7 @@ export const isRegressionEvaluateResponse = (arg: any): arg is RegressionEvaluat return ( keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION && - arg?.regression?.mean_squared_error !== undefined && + arg?.regression?.mse !== undefined && arg?.regression?.r_squared !== undefined ); }; @@ -410,7 +410,7 @@ export const useRefreshAnalyticsList = ( const DEFAULT_SIG_FIGS = 3; export function getValuesFromResponse(response: RegressionEvaluateResponse) { - let meanSquaredError = response?.regression?.mean_squared_error?.value; + let meanSquaredError = response?.regression?.mse?.value; if (meanSquaredError) { meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS)); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 8b30dccc25306..898e29a303881 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -560,7 +560,7 @@ function calculateChartRange( // Calculate the time range for the charts. // Fit in as many points in the available container width plotted at the job bucket span. const midpointMs = Math.ceil((earliestMs + latestMs) / 2); - const maxBucketSpanMs = Math.max.apply(null, _.pluck(seriesConfigs, 'bucketSpanSeconds')) * 1000; + const maxBucketSpanMs = Math.max.apply(null, _.map(seriesConfigs, 'bucketSpanSeconds')) * 1000; const pointsToPlotFullSelection = Math.ceil((latestMs - earliestMs) / maxBucketSpanMs); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.js b/x-pack/plugins/ml/public/application/util/string_utils.js index 450c166f90300..7411820ba3239 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.js +++ b/x-pack/plugins/ml/public/application/util/string_utils.js @@ -91,7 +91,7 @@ export function sortByKey(list, reverse, comparator) { keys = keys.reverse(); } - return _.object( + return _.zipObject( keys, _.map(keys, (key) => { return list[key]; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 1ed9df8da65dd..ae9a56f00a5c1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -362,7 +362,7 @@ export class DataRecognizer { // takes a module config id, an optional jobPrefix and the request object // creates all of the jobs, datafeeds and savedObjects listed in the module config. // if any of the savedObjects already exist, they will not be overwritten. - async setupModuleItems( + async setup( moduleId: string, jobPrefix?: string, groups?: string[], diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 78cc7901363cd..d58c797b446db 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -143,7 +143,7 @@ export class DataVisualizer { // split the check into multiple batches (max 200 fields per request). const batches: string[][] = [[]]; _.each(aggregatableFields, (field) => { - let lastArray: string[] = _.last(batches); + let lastArray: string[] = _.last(batches) as string[]; if (lastArray.length === AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE) { lastArray = []; batches.push(lastArray); @@ -229,7 +229,7 @@ export class DataVisualizer { if (batchedFields[fieldType] === undefined) { batchedFields[fieldType] = [[]]; } - let lastArray: Field[] = _.last(batchedFields[fieldType]); + let lastArray: Field[] = _.last(batchedFields[fieldType]) as Field[]; if (lastArray.length === FIELDS_REQUEST_BATCH_SIZE) { lastArray = []; batchedFields[fieldType].push(lastArray); @@ -867,7 +867,7 @@ export class DataVisualizer { [...aggsPath, `${safeFieldName}_values`, 'buckets'], [] ); - _.each(valueBuckets, (bucket) => { + _.forEach(valueBuckets, (bucket) => { stats[`${bucket.key_as_string}Count`] = bucket.doc_count; }); @@ -958,7 +958,7 @@ export class DataVisualizer { // Look ahead to the last percentiles and process these too if // they don't add more than 50% to the value range. - const lastValue = _.last(percentileBuckets).value; + const lastValue = (_.last(percentileBuckets) as any).value; const upperBound = lowerBound + 1.5 * (lastValue - lowerBound); const filteredLength = percentileBuckets.length; for (let i = filteredLength; i < percentiles.length; i++) { @@ -979,7 +979,7 @@ export class DataVisualizer { // Add in 0-5 and 95-100% if they don't add more // than 25% to the value range at either end. - const lastValue: number = _.last(percentileBuckets).value; + const lastValue: number = (_.last(percentileBuckets) as any).value; const maxDiff = 0.25 * (lastValue - lowerBound); if (lowerBound - dataMin < maxDiff) { percentileBuckets.splice(0, 0, percentiles[0]); diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index ade3d3eca90ea..88d24a1b86b6d 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -38,7 +38,7 @@ function getModule(context: RequestHandlerContext, moduleId: string) { } } -function saveModuleItems( +function setup( context: RequestHandlerContext, moduleId: string, prefix?: string, @@ -57,7 +57,7 @@ function saveModuleItems( context.ml!.mlClient.callAsCurrentUser, context.core.savedObjects.client ); - return dr.setupModuleItems( + return dr.setup( moduleId, prefix, groups, @@ -438,7 +438,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { estimateModelMemory, } = request.body as TypeOf; - const result = await saveModuleItems( + const result = await setup( context, moduleId, prefix, diff --git a/x-pack/plugins/ml/server/routes/schemas/modules.ts b/x-pack/plugins/ml/server/routes/schemas/modules.ts index 23148c14c734e..e2b58cf2ce8f2 100644 --- a/x-pack/plugins/ml/server/routes/schemas/modules.ts +++ b/x-pack/plugins/ml/server/routes/schemas/modules.ts @@ -71,19 +71,19 @@ export const setupModuleBodySchema = schema.object({ estimateModelMemory: schema.maybe(schema.boolean()), }); -export const getModuleIdParamSchema = (optional = false) => { - const stringType = schema.string(); - return schema.object({ - /** - * ID of the module. - */ - moduleId: optional ? schema.maybe(stringType) : stringType, - }); -}; - -export const optionalModuleIdParamSchema = getModuleIdParamSchema(true); +export const optionalModuleIdParamSchema = schema.object({ + /** + * ID of the module. + */ + moduleId: schema.maybe(schema.string()), +}); -export const moduleIdParamSchema = getModuleIdParamSchema(false); +export const moduleIdParamSchema = schema.object({ + /** + * ID of the module. + */ + moduleId: schema.string(), +}); export const modulesIndexPatternTitleSchema = schema.object({ /** diff --git a/x-pack/plugins/ml/server/shared.ts b/x-pack/plugins/ml/server/shared.ts index 7b4b2a55c29f1..3fca8ea1ba047 100644 --- a/x-pack/plugins/ml/server/shared.ts +++ b/x-pack/plugins/ml/server/shared.ts @@ -7,3 +7,4 @@ export * from '../common/types/anomalies'; export * from '../common/types/anomaly_detection_jobs'; export * from './lib/capabilities/errors'; +export { ModuleSetupPayload } from './shared_services/providers/modules'; diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts index d1666c5c1bf7b..27935fd6fe21d 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts @@ -5,8 +5,13 @@ */ import { LegacyAPICaller, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; import { DataRecognizer } from '../../models/data_recognizer'; import { SharedServicesChecks } from '../shared_services'; +import { moduleIdParamSchema, setupModuleBodySchema } from '../../routes/schemas/modules'; + +export type ModuleSetupPayload = TypeOf & + TypeOf; export interface ModulesProvider { modulesProvider( @@ -17,7 +22,7 @@ export interface ModulesProvider { recognize: DataRecognizer['findMatches']; getModule: DataRecognizer['getModule']; listModules: DataRecognizer['listModules']; - setupModuleItems: DataRecognizer['setupModuleItems']; + setup(payload: ModuleSetupPayload): ReturnType; }; } @@ -52,11 +57,24 @@ export function getModulesProvider({ return dr.listModules(); }, - async setupModuleItems(...args) { + async setup(payload: ModuleSetupPayload) { isFullLicense(); await hasMlCapabilities(['canCreateJob']); - return dr.setupModuleItems(...args); + return dr.setup( + payload.moduleId, + payload.prefix, + payload.groups, + payload.indexPatternName, + payload.query, + payload.useDedicatedIndex, + payload.startDatafeed, + payload.start, + payload.end, + payload.jobOverrides, + payload.datafeedOverrides, + payload.estimateModelMemory + ); }, }; }, diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 726d4be4924d7..9ebb074ec7c3b 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -10,7 +10,7 @@ import '../views/all'; import 'angular-sanitize'; import 'angular-route'; import '../index.scss'; -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; @@ -159,7 +159,7 @@ function createMonitoringAppFilters() { .module('monitoring/filters', []) .filter('capitalize', function () { return function (input: string) { - return capitalize(input?.toLowerCase()); + return upperFirst(input?.toLowerCase()); }; }) .filter('formatNumber', function () { diff --git a/x-pack/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js index b3fc70e9ffd7d..59e838c449a3b 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/plugins/monitoring/public/components/alerts/alerts.js @@ -6,7 +6,7 @@ import React from 'react'; import { Legacy } from '../../legacy_shims'; -import { capitalize, get } from 'lodash'; +import { upperFirst, get } from 'lodash'; import { formatDateTimeLocal } from '../../../common/formatting'; import { formatTimestampToDuration } from '../../../common'; import { @@ -55,7 +55,7 @@ const getColumns = (timezone) => [ data-test-subj="alertIcon" aria-label={severityIcon.title} > - {capitalize(severityIcon.value)} + {upperFirst(severityIcon.value)} ); diff --git a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js index b76f4eb5b75a7..8232e0a8908d0 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js +++ b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; /** * Map the {@code severity} value to the associated alert level to be usable within the UI. @@ -68,7 +68,7 @@ export function mapSeverity(severity) { return { title: i18n.translate('xpack.monitoring.alerts.severityTitle', { defaultMessage: '{severity} severity alert', - values: { severity: capitalize(mapped.value) }, + values: { severity: upperFirst(mapped.value) }, }), ...mapped, }; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js index 9c5981585a8de..9acfce1e8c0b9 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { capitalize, find, get, includes } from 'lodash'; +import { upperFirst, find, get, includes } from 'lodash'; import { i18n } from '@kbn/i18n'; export function decorateShards(shards, nodes) { @@ -40,7 +40,7 @@ export function decorateShards(shards, nodes) { ); } } - return capitalize(shard.state.toLowerCase()); + return upperFirst(shard.state.toLowerCase()); } return shards.map((shard) => { diff --git a/x-pack/plugins/monitoring/public/components/logs/logs.js b/x-pack/plugins/monitoring/public/components/logs/logs.js index 0ab3683f4b72f..297ce49f1f148 100644 --- a/x-pack/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { PureComponent } from 'react'; -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import { Legacy } from '../../legacy_shims'; import { EuiBasicTable, EuiTitle, EuiSpacer, EuiText, EuiCallOut, EuiLink } from '@elastic/eui'; import { INFRA_SOURCE_ID } from '../../../common/constants'; @@ -59,7 +59,7 @@ const columns = [ field: 'type', name: columnTypeTitle, width: '10%', - render: (type) => capitalize(type), + render: (type) => upperFirst(type), }, { field: 'message', @@ -89,7 +89,7 @@ const clusterColumns = [ field: 'type', name: columnTypeTitle, width: '10%', - render: (type) => capitalize(type), + render: (type) => upperFirst(type), }, { field: 'message', diff --git a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx index 14f838cff7a3c..12bd3a7575cf2 100644 --- a/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx +++ b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { contains } from 'lodash'; +import { includes } from 'lodash'; import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../legacy_shims'; @@ -38,7 +38,7 @@ export function ajaxErrorHandlersProvider() { if (err.status === 403) { // redirect to error message view history.replaceState(null, '', '#/access-denied'); - } else if (err.status === 404 && !contains(window.location.hash, 'no-data')) { + } else if (err.status === 404 && !includes(window.location.hash, 'no-data')) { // pass through if this is a 404 and we're already on the no-data page Legacy.shims.toastNotifications.addDanger({ title: toMountPoint( diff --git a/x-pack/plugins/monitoring/public/lib/form_validation.ts b/x-pack/plugins/monitoring/public/lib/form_validation.ts index 98d56f9790be4..2255022dcece0 100644 --- a/x-pack/plugins/monitoring/public/lib/form_validation.ts +++ b/x-pack/plugins/monitoring/public/lib/form_validation.ts @@ -5,13 +5,13 @@ */ import { i18n } from '@kbn/i18n'; -import { isString, isNumber, capitalize } from 'lodash'; +import { isString, isNumber, upperFirst } from 'lodash'; export function getRequiredFieldError(field: string): string { return i18n.translate('xpack.monitoring.alerts.migrate.manageAction.requiredFieldError', { defaultMessage: '{field} is a required field.', values: { - field: capitalize(field), + field: upperFirst(field), }, }); } diff --git a/x-pack/plugins/monitoring/public/lib/route_init.js b/x-pack/plugins/monitoring/public/lib/route_init.js index 9467535d556b0..163688d772022 100644 --- a/x-pack/plugins/monitoring/public/lib/route_init.js +++ b/x-pack/plugins/monitoring/public/lib/route_init.js @@ -13,7 +13,7 @@ export function routeInitProvider(Private, monitoringClusters, globalState, lice const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); function isOnPage(hash) { - return _.contains(window.location.hash, hash); + return _.includes(window.location.hash, hash); } /* diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 5afb382b7cda8..2a4caf17515e1 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -6,14 +6,14 @@ import React from 'react'; import { render } from 'react-dom'; -import { get, contains } from 'lodash'; +import { get, includes } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Legacy } from '../legacy_shims'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; function isOnPage(hash: string) { - return contains(window.location.hash, hash); + return includes(window.location.hash, hash); } interface IAngularState { diff --git a/x-pack/plugins/monitoring/public/services/license.js b/x-pack/plugins/monitoring/public/services/license.js index 341309004b110..caa21cd8ee8d7 100644 --- a/x-pack/plugins/monitoring/public/services/license.js +++ b/x-pack/plugins/monitoring/public/services/license.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { contains } from 'lodash'; +import { includes } from 'lodash'; import { ML_SUPPORTED_LICENSES } from '../../common/constants'; export function licenseProvider() { @@ -27,7 +27,7 @@ export function licenseProvider() { } mlIsSupported() { - return contains(ML_SUPPORTED_LICENSES, this.license.type); + return includes(ML_SUPPORTED_LICENSES, this.license.type); } doesExpire() { diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js index c3fbe266be6d6..cc3682ef764c8 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, capitalize } from 'lodash'; +import { get, upperFirst } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createQuery } from '../create_query'; import { getDiffCalculation } from '../beats/_beats_stats'; @@ -33,8 +33,8 @@ export function handleResponse(response, apmUuid) { transportAddress: get(stats, 'beat.host', null), version: get(stats, 'beat.version', null), name: get(stats, 'beat.name', null), - type: capitalize(get(stats, 'beat.type')) || null, - output: capitalize(get(stats, 'metrics.libbeat.output.type')) || null, + type: upperFirst(get(stats, 'beat.type')) || null, + output: upperFirst(get(stats, 'metrics.libbeat.output.type')) || null, configReloads: get(stats, 'metrics.libbeat.config.reloads', null), uptime: get(stats, 'metrics.beat.info.uptime.ms', null), eventsTotal: getDiffCalculation(eventsTotalLast, eventsTotalFirst), diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js index 40070a6b0d0f2..19ed8298391d7 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.js @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { capitalize, get } from 'lodash'; +import { upperFirst, get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createApmQuery } from './create_apm_query'; import { calculateRate } from '../calculate_rate'; @@ -59,8 +59,8 @@ export function handleResponse(response, start, end) { accum.beats.push({ uuid: get(stats, 'beat.uuid'), name: get(stats, 'beat.name'), - type: capitalize(get(stats, 'beat.type')), - output: capitalize(get(stats, 'metrics.libbeat.output.type')), + type: upperFirst(get(stats, 'beat.type')), + output: upperFirst(get(stats, 'metrics.libbeat.output.type')), total_events_rate: totalEventsRate, bytes_sent_rate: bytesSentRate, errors, diff --git a/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.js b/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.js index cf5a99525cc4c..9508260a64139 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { capitalize, get } from 'lodash'; +import { upperFirst, get } from 'lodash'; export const getDiffCalculation = (max, min) => { // no need to test max >= 0, but min <= 0 which is normal for a derivative after restart @@ -105,7 +105,7 @@ export const beatsAggResponseHandler = (response) => { return [ ...types, { - type: capitalize(typeBucket.key), + type: upperFirst(typeBucket.key), count: get(typeBucket, 'uuids.buckets.length'), }, ]; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js index 06f6cf4f1a5e0..30ec728546ce9 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { capitalize, get } from 'lodash'; +import { upperFirst, get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createBeatsQuery } from './create_beats_query.js'; import { getDiffCalculation } from './_beats_stats'; @@ -33,8 +33,8 @@ export function handleResponse(response, beatUuid) { transportAddress: get(stats, 'beat.host', null), version: get(stats, 'beat.version', null), name: get(stats, 'beat.name', null), - type: capitalize(get(stats, 'beat.type')) || null, - output: capitalize(get(stats, 'metrics.libbeat.output.type')) || null, + type: upperFirst(get(stats, 'beat.type')) || null, + output: upperFirst(get(stats, 'metrics.libbeat.output.type')) || null, configReloads: get(stats, 'metrics.libbeat.config.reloads', null), uptime: get(stats, 'metrics.beat.info.uptime.ms', null), eventsTotal: getDiffCalculation(eventsTotalLast, eventsTotalFirst), diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats.js index ef878e4892557..a5d43d1da7ebc 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.js @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { capitalize, get } from 'lodash'; +import { upperFirst, get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createBeatsQuery } from './create_beats_query'; import { calculateRate } from '../calculate_rate'; @@ -59,8 +59,8 @@ export function handleResponse(response, start, end) { accum.beats.push({ uuid: get(stats, 'beat.uuid'), name: get(stats, 'beat.name'), - type: capitalize(get(stats, 'beat.type')), - output: capitalize(get(stats, 'metrics.libbeat.output.type')), + type: upperFirst(get(stats, 'beat.type')), + output: upperFirst(get(stats, 'metrics.libbeat.output.type')), total_events_rate: totalEventsRate, bytes_sent_rate: bytesSentRate, errors, diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js index f630903d4e29d..10a75b9d1ca85 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { capitalize, get } from 'lodash'; +import { upperFirst, get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createBeatsQuery } from './create_beats_query'; @@ -47,7 +47,7 @@ export function handleResponse(response) { return [ ...accum, { - type: capitalize(current.key), + type: upperFirst(current.key), count: get(current, 'uuids.buckets.length'), }, ]; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js index c6575393590f0..74d4bd6d2b5df 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.js @@ -5,7 +5,7 @@ */ import Bluebird from 'bluebird'; -import { contains, get } from 'lodash'; +import { includes, get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createQuery } from '../create_query'; import { ElasticsearchMetric } from '../metrics'; @@ -59,7 +59,7 @@ export function getMlJobs(req, esIndexPattern) { export function getMlJobsForCluster(req, esIndexPattern, cluster) { const license = get(cluster, 'license', {}); - if (license.status === 'active' && contains(ML_SUPPORTED_LICENSES, license.type)) { + if (license.status === 'active' && includes(ML_SUPPORTED_LICENSES, license.type)) { // ML is supported const start = req.payload.timeRange.min; // no wrapping in moment :) const end = req.payload.timeRange.max; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.js index 4e5e439ff90d5..d1a7aec2f153f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, pluck, min, max, last } from 'lodash'; +import { get, map, min, max, last } from 'lodash'; import { filterPartialBuckets } from '../../../filter_partial_buckets'; import { metrics } from '../../../metrics'; @@ -76,14 +76,14 @@ function reduceMetric(metricName, metricBuckets, { min: startTime, max: endTime, /* it's possible that no data exists for the type of metric. For example, * node_cgroup_throttled data could be completely null if there is no cgroup * throttling. */ - const allValues = pluck(mappedData, 'y'); + const allValues = map(mappedData, 'y'); if (allValues.join(',') === '') { return; // no data exists for this type of metric } - const minVal = min(pluck(mappedData, 'y')); - const maxVal = max(pluck(mappedData, 'y')); - const lastVal = last(pluck(mappedData, 'y')); + const minVal = min(map(mappedData, 'y')); + const maxVal = max(map(mappedData, 'y')); + const lastVal = last(map(mappedData, 'y')); const slope = calcSlope(mappedData) > 0 ? 1 : -1; // no need for the entire precision, it's just an up/down arrow return { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.js index 8782875775530..39855e7f10eaf 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.js @@ -3,12 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; export function sortNodes(nodes, sort) { if (!sort || !sort.field) { return nodes; } - return sortByOrder(nodes, (node) => node[sort.field], sort.direction); + return orderBy(nodes, (node) => node[sort.field], sort.direction); } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.js b/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.js index 2a5c15ece4b40..e4a36fdf35daf 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.js @@ -3,12 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; export function sortPipelines(pipelines, sort) { if (!sort) { return pipelines; } - return sortByOrder(pipelines, (pipeline) => pipeline[sort.field], sort.direction); + return orderBy(pipelines, (pipeline) => pipeline[sort.field], sort.direction); } 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 e65d1779520cf..e57dfebb36419 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 @@ -59,7 +59,6 @@ export interface MetricsFetchDataResponse extends FetchDataResponse { hosts: Stat; cpu: Stat; memory: Stat; - disk: Stat; inboundTraffic: Stat; outboundTraffic: Stat; }; diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts index d7a708173d3ad..27913fafe3257 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts @@ -40,7 +40,7 @@ async function getStats(callCluster: LegacyAPICaller, index: string) { }, }; const esResponse = await callCluster('search', searchParams); - const size = _.get(esResponse, 'hits.hits.length'); + const size = _.get(esResponse, 'hits.hits.length') as number; if (size < 1) { return; } diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 37441edce0af8..d0800c7b24fef 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -5,6 +5,7 @@ */ import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 898b123e976fd..bca9496bc9add 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { map, trunc } from 'lodash'; +import { map, truncate } from 'lodash'; import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; @@ -70,7 +70,7 @@ export class HeadlessChromiumDriver { } private truncateUrl(url: string) { - return trunc(url, { + return truncate(url, { length: 100, omission: '[truncated]', }); diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 5c66bd599dd9a..3c892fe6120af 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n/'; import crypto from 'crypto'; -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import { Observable } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; import { CoreSetup } from 'src/core/server'; @@ -84,7 +84,7 @@ export function createConfig$( // disableSandbox was not set by user, apply default for OS const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled(); - const osName = [os.os, os.dist, os.release].filter(Boolean).map(capitalize).join(' '); + const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' '); logger.debug( i18n.translate('xpack.reporting.serverConfig.osDetected', { diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts index db7137c30513b..305fb6bab5478 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.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 { omit } from 'lodash'; +import { omitBy } from 'lodash'; import { KBN_SCREENSHOT_HEADER_BLACKLIST, KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, @@ -16,7 +16,7 @@ export const omitBlacklistedHeaders = ({ job: ScheduledTaskParamsType; decryptedHeaders: Record; }) => { - const filteredHeaders: Record = omit( + const filteredHeaders: Record = omitBy( decryptedHeaders, (_value, header: string) => header && diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.ts b/x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.ts index a4c634439ec45..acfae5138154c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/server/lib/flatten_hit.ts @@ -30,7 +30,7 @@ export function createFlattenHit( } else if (_.isArray(flat[key])) { flat[key].push(val); } else { - flat[key] = [flat[key], val]; + flat[key] = [flat[key], val] as any; } return; } @@ -49,7 +49,7 @@ export function createFlattenHit( const flattenFields = (flat: FlatHits, hitFields: string[]) => { _.forOwn(hitFields, (val, key) => { if (key) { - if (key[0] === '_' && !_.contains(metaFields, key)) return; + if (key[0] === '_' && !_.includes(metaFields, key)) return; flat[key] = _.isArray(val) && val.length === 1 ? val[0] : val; } }); diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 2a6d08c0740dd..213bea3bc3eec 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -13,11 +13,9 @@ import { decorateRangeStats } from './decorate_range_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import { AggregationResultBuckets, - AppCounts, FeatureAvailabilityMap, JobTypes, KeyCountBucket, - LayoutCounts, RangeStats, ReportingUsageType, SearchResponse, @@ -75,21 +73,21 @@ function getAggStats(aggs: AggregationResultBuckets): Partial { // merge pdf stats into pdf jobtype key const pdfJobs = jobTypes[PRINTABLE_PDF_JOBTYPE]; if (pdfJobs) { - const pdfAppBuckets = get(aggs[OBJECT_TYPES_KEY], '.pdf.buckets', []); - const pdfLayoutBuckets = get(aggs[LAYOUT_TYPES_KEY], '.pdf.buckets', []); - pdfJobs.app = getKeyCount(pdfAppBuckets); - pdfJobs.layout = getKeyCount(pdfLayoutBuckets); + const pdfAppBuckets = get(aggs[OBJECT_TYPES_KEY], 'pdf.buckets', []); + const pdfLayoutBuckets = get(aggs[LAYOUT_TYPES_KEY], 'pdf.buckets', []); + pdfJobs.app = getKeyCount(pdfAppBuckets); + pdfJobs.layout = getKeyCount(pdfLayoutBuckets); } const all = aggs.doc_count; let statusTypes = {}; - const statusBuckets = get(aggs[STATUS_TYPES_KEY], 'buckets', []); + const statusBuckets = get(aggs[STATUS_TYPES_KEY], 'buckets', []); if (statusBuckets) { statusTypes = getKeyCount(statusBuckets); } let statusByApp = {}; - const statusAppBuckets = get(aggs[STATUS_BY_APP_KEY], 'buckets', []); + const statusAppBuckets = get(aggs[STATUS_BY_APP_KEY], 'buckets', []); if (statusAppBuckets) { statusByApp = getAppStatuses(statusAppBuckets); } @@ -97,18 +95,16 @@ function getAggStats(aggs: AggregationResultBuckets): Partial { return { _all: all, status: statusTypes, statuses: statusByApp, ...jobTypes }; } -type SearchAggregation = SearchResponse['aggregations']['ranges']['buckets']; - type RangeStatSets = Partial & { last7Days: Partial; }; async function handleResponse(response: SearchResponse): Promise> { - const buckets = get(response, 'aggregations.ranges.buckets'); + const buckets = get(response, 'aggregations.ranges.buckets'); if (!buckets) { return {}; } - const { last7Days, all } = buckets; + const { last7Days, all } = buckets as any; const last7DaysUsage = last7Days ? getAggStats(last7Days) : {}; const allUsage = all ? getAggStats(all) : {}; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js index 151eff31f8a01..8c326c3f8a788 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js @@ -6,10 +6,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; -import mapValues from 'lodash/object/mapValues'; -import cloneDeep from 'lodash/lang/cloneDeep'; -import debounce from 'lodash/function/debounce'; -import first from 'lodash/array/first'; +import { cloneDeep, debounce, first, mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js index d70fbb89c065d..df9b63bc5fa3d 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js @@ -8,7 +8,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import get from 'lodash/object/get'; +import { get } from 'lodash'; import { EuiButtonEmpty, diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 3a61518a850e9..56225639777cd 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import cloneDeep from 'lodash/lang/cloneDeep'; -import get from 'lodash/object/get'; -import pick from 'lodash/object/pick'; +import { cloneDeep, get, pick } from 'lodash'; import { WEEK } from '../../../../../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js index aa95bbbd9cf0d..3ebc7e5c8192c 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js @@ -9,7 +9,10 @@ import { mockHttpRequest, pageHelpers, nextTick } from './helpers'; import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); const { setup } = pageHelpers.jobClone; const { diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js index 8791b5173b893..90f53a91e425a 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js @@ -10,7 +10,10 @@ import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); const { setup } = pageHelpers.jobCreate; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js index 50898f94586fc..549f6ab063746 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js @@ -8,7 +8,10 @@ import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); const { setup } = pageHelpers.jobCreate; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js index a1edf87c33bad..6cf33334d9281 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js @@ -17,7 +17,10 @@ import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); const { setup } = pageHelpers.jobCreate; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js index 7f58482d35b19..d75c7b585994c 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js @@ -8,7 +8,10 @@ import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); const { setup } = pageHelpers.jobCreate; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js index 59118ef6f8ec3..3dbbe70bfc560 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js @@ -10,7 +10,10 @@ import { setHttp } from '../../crud_app/services'; import { JOBS } from './helpers/constants'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); jest.mock('../../kibana_services', () => { const services = require.requireActual('../../kibana_services'); diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js index f21fc2c12a007..9434747028e53 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js @@ -8,7 +8,10 @@ import { setHttp } from '../../crud_app/services'; import { pageHelpers, mockHttpRequest } from './helpers'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); const { setup } = pageHelpers.jobCreate; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js index 53a3af38f3235..76be39a2c0e09 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js @@ -10,7 +10,10 @@ import { getRouter } from '../../crud_app/services/routing'; import { setHttp } from '../../crud_app/services'; import { coreMock } from '../../../../../../src/core/public/mocks'; -jest.mock('lodash/function/debounce', () => (fn) => fn); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); jest.mock('../../kibana_services', () => { const services = require.requireActual('../../kibana_services'); diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts index c679098bc05be..35c40e42efc19 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -48,7 +48,7 @@ async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCl const esResponse = await callCluster('search', searchParams); - return get(esResponse, 'hits.hits', []).map((indexPattern) => { + return get(esResponse, 'hits.hits', []).map((indexPattern: any) => { const { _id: savedObjectId } = indexPattern; return getIdFromSavedObjectId(savedObjectId); }); @@ -81,7 +81,7 @@ async function fetchRollupSavedSearches( const savedSearches = get(esResponse, 'hits.hits', []); // Filter for ones with rollup index patterns. - return savedSearches.reduce((rollupSavedSearches, savedSearch) => { + return savedSearches.reduce((rollupSavedSearches: any, savedSearch: any) => { const { _id: savedObjectId, _source: { @@ -136,7 +136,7 @@ async function fetchRollupVisualizations( let rollupVisualizations = 0; let rollupVisualizationsFromSavedSearches = 0; - visualizations.forEach((visualization) => { + visualizations.forEach((visualization: any) => { const { _source: { visualization: { @@ -151,7 +151,7 @@ async function fetchRollupVisualizations( if (savedSearchRefName) { // This visualization depends upon a saved search. - const savedSearch = references.find((ref) => ref.name === savedSearchRefName); + const savedSearch = references.find((ref: any) => ref.name === savedSearchRefName); if (rollupSavedSearchesToFlagMap[savedSearch.id]) { rollupVisualizations++; rollupVisualizationsFromSavedSearches++; diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts index 815fe163411b3..885836780f1a9 100644 --- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.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 { indexBy, isString } from 'lodash'; +import { keyBy, isString } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { CallWithRequestFactoryShim } from '../../types'; @@ -74,7 +74,7 @@ export const getRollupSearchStrategy = ( }: { fieldsCapabilities: { [key: string]: any }; rollupIndex: string } ) { const fields = await super.getFieldsForWildcard(req, indexPattern); - const fieldsFromFieldCapsApi = indexBy(fields, 'name'); + const fieldsFromFieldCapsApi = keyBy(fields, 'name'); const rollupIndexCapabilities = fieldsCapabilities[rollupIndex].aggs; return mergeCapabilitiesWithFields(rollupIndexCapabilities, fieldsFromFieldCapsApi); diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts index 546d9d628277f..250947d72c5fa 100644 --- a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; import { schema } from '@kbn/config-schema'; import { Field } from '../../../lib/merge_capabilities_with_fields'; import { RouteDependencies } from '../../../types'; @@ -111,7 +111,7 @@ export const registerFieldsForWildcardRoute = ({ const parsedParams = JSON.parse(params); const rollupIndex = parsedParams.rollup_index; const rollupFields: Field[] = []; - const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name'); + const fieldsFromFieldCapsApi: { [key: string]: any } = keyBy(fields, 'name'); const rollupIndexCapabilities = getCapabilitiesForRollupIndices( await context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { indexPattern: rollupIndex, diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/init_data.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/init_data.ts index f5daa6b38de48..e4dded600dcf2 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/init_data.ts +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/init_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import cloneDeep from 'lodash.clonedeep'; +import { cloneDeep } from 'lodash'; import { flow } from 'fp-ts/lib/function'; import { Targets, Shard, ShardSerialized } from '../../types'; import { calcTimes, initTree, normalizeIndices, sortIndices } from './unsafe_utils'; @@ -108,7 +108,7 @@ export const normalize = (target: Targets) => (data: IndexMap) => { export const initDataFor = (target: Targets) => flow( - cloneDeep, + cloneDeep as any, initShards, calculateShardValues(target), initIndices, diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/unsafe_utils.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/unsafe_utils.ts index b023f1b365c06..0fb0522d449bd 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/unsafe_utils.ts +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/unsafe_utils.ts @@ -122,10 +122,10 @@ export function normalizeIndices(indices: IndexMap, target: Targets) { let sortQueryComponents; if (target === 'searches') { sortQueryComponents = (a: Shard, b: Shard) => { - const aTime = _.sum(a.searches!, (search) => { + const aTime = _.sumBy(a.searches!, (search: any) => { return search.treeRoot!.time; }); - const bTime = _.sum(b.searches!, (search) => { + const bTime = _.sumBy(b.searches!, (search: any) => { return search.treeRoot!.time; }); @@ -133,10 +133,10 @@ export function normalizeIndices(indices: IndexMap, target: Targets) { }; } else if (target === 'aggregations') { sortQueryComponents = (a: Shard, b: Shard) => { - const aTime = _.sum(a.aggregations!, (agg) => { + const aTime = _.sumBy(a.aggregations!, (agg: any) => { return agg.treeRoot!.time; }); - const bTime = _.sum(b.aggregations!, (agg) => { + const bTime = _.sumBy(b.aggregations!, (agg: any) => { return agg.treeRoot!.time; }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx index 10aa59083dff6..14375587c8497 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx @@ -51,7 +51,7 @@ export class ChangeAllPrivilegesControl extends Component { }} disabled={this.props.disabled} > - {_.capitalize(privilege.id)} + {_.upperFirst(privilege.id)} ); }); @@ -65,7 +65,7 @@ export class ChangeAllPrivilegesControl extends Component { }} disabled={this.props.disabled} > - {_.capitalize(NO_PRIVILEGE_VALUE)} + {_.upperFirst(NO_PRIVILEGE_VALUE)} ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index 38e4390a2856a..a371a9ec9ba1e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -15,7 +15,6 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; import React, { Component } from 'react'; import { Role } from '../../../../../../../common/model'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index 7b5d8d8c1ed27..204fb512abcff 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -40,7 +40,7 @@ function getDisplayValue(privilege: string | string[] | undefined) { if (isPrivilegeMissing) { displayValue = ; } else { - displayValue = privileges.map((p) => _.capitalize(p)).join(', '); + displayValue = privileges.map((p) => _.upperFirst(p)).join(', '); } return displayValue; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 585c07c2e834f..64b7fe3e2e3a9 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -20,7 +20,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import _ from 'lodash'; import React, { Component } from 'react'; import { Space, getSpaceColor } from '../../../../../../../../spaces/public'; import { FeaturesPrivileges, Role, copyRole } from '../../../../../../../common/model'; diff --git a/x-pack/plugins/security/public/management/roles/model/kibana_privilege.ts b/x-pack/plugins/security/public/management/roles/model/kibana_privilege.ts index f5c85d3d92be2..cc9e74805040c 100644 --- a/x-pack/plugins/security/public/management/roles/model/kibana_privilege.ts +++ b/x-pack/plugins/security/public/management/roles/model/kibana_privilege.ts @@ -10,7 +10,7 @@ export class KibanaPrivilege { constructor(public readonly id: string, public readonly actions: string[] = []) {} public get name() { - return _.capitalize(this.id); + return _.upperFirst(this.id); } public grantsPrivilege(candidatePrivilege: KibanaPrivilege) { diff --git a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 7a97794303558..296a8f6c8693f 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -12,6 +12,7 @@ import { EuiForm, EuiFormRow, } from '@elastic/eui'; +import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts index 448b7b7e7ef48..8b5c119d59494 100644 --- a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual, difference } from 'lodash'; +import { isEqual, isEqualWith, difference } from 'lodash'; import { ILegacyClusterClient, Logger } from '../../../../../src/core/server'; import { serializePrivileges } from './privileges_serializer'; @@ -22,7 +22,7 @@ export async function registerPrivilegesWithCluster( ) => { // when comparing privileges, the order of the actions doesn't matter, lodash's isEqual // doesn't know how to compare Sets - return isEqual(existingPrivileges, expectedPrivileges, (value, other, key) => { + return isEqualWith(existingPrivileges, expectedPrivileges, (value, other, key) => { if (key === 'actions' && Array.isArray(value) && Array.isArray(other)) { // Array.sort() is in-place, and we don't want to be modifying the actual order // of the arrays permanently, and there's potential they're frozen, so we're copying diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 199b8a91e4307..37b7308856196 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -25,22 +25,8 @@ export const factory = (): PolicyConfig => { mode: ProtectionModes.prevent, }, logging: { - stdout: 'debug', file: 'info', }, - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { - connect: true, - process: true, - }, - }, - }, }, mac: { events: { @@ -49,25 +35,11 @@ export const factory = (): PolicyConfig => { network: true, }, malware: { - mode: ProtectionModes.detect, + mode: ProtectionModes.prevent, }, logging: { - stdout: 'debug', file: 'info', }, - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { - connect: true, - process: true, - }, - }, - }, }, linux: { events: { @@ -76,22 +48,8 @@ export const factory = (): PolicyConfig => { network: true, }, logging: { - stdout: 'debug', file: 'info', }, - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { - connect: true, - process: true, - }, - }, - }, }, }; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 42b1337a91464..f2b8acb627cc4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -613,10 +613,8 @@ export interface PolicyConfig { }; malware: MalwareFields; logging: { - stdout: string; file: string; }; - advanced: PolicyConfigAdvancedOptions; }; mac: { events: { @@ -626,10 +624,8 @@ export interface PolicyConfig { }; malware: MalwareFields; logging: { - stdout: string; file: string; }; - advanced: PolicyConfigAdvancedOptions; }; linux: { events: { @@ -638,10 +634,8 @@ export interface PolicyConfig { network: boolean; }; logging: { - stdout: string; file: string; }; - advanced: PolicyConfigAdvancedOptions; }; } @@ -663,20 +657,6 @@ export interface UIPolicyConfig { linux: Pick; } -interface PolicyConfigAdvancedOptions { - elasticsearch: { - indices: { - control: string; - event: string; - logging: string; - }; - kernel: { - connect: boolean; - process: boolean; - }; - }; -} - /** Policy: Malware protection fields */ export interface MalwareFields { mode: ProtectionModes; @@ -686,7 +666,6 @@ export interface MalwareFields { export enum ProtectionModes { detect = 'detect', prevent = 'prevent', - preventNotify = 'preventNotify', off = 'off', } diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 1ce5243bf7950..703ef6584f164 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -13,13 +13,11 @@ "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" }, "devDependencies": { - "@types/lodash": "^4.14.110", "@types/md5": "^2.2.0" }, "dependencies": { "@types/rbush": "^3.0.0", "@types/seedrandom": ">=2.0.0 <4.0.0", - "lodash": "^4.17.15", "querystring": "^0.2.0", "rbush": "^3.0.1", "redux-devtools-extension": "^2.13.8" diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts index a3972fd35bf2d..4b57c7dc20d9f 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts @@ -10,6 +10,7 @@ export const buildLastAlertsQuery = (ruleId: string | undefined | null) => { bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 }, }, ]; + return { aggs: { lastSeen: { max: { field: '@timestamp' } }, @@ -30,7 +31,7 @@ export const buildLastAlertsQuery = (ruleId: string | undefined | null) => { : queryFilter, }, }, - size: 0, + size: 1, track_total_hits: true, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index f079715baec1c..a3cab1cfabd71 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -376,4 +376,63 @@ describe('QueryBar ', () => { expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); }); }); + + describe('SavedQueryManagementComponent state', () => { + test('popover should hidden when "Save current query" button was clicked', () => { + const KibanaWithStorageProvider = createKibanaContextProviderMock(); + + const Proxy = (props: QueryBarComponentProps) => ( + + + + + + ); + + const wrapper = mount( + + ); + + const isSavedQueryPopoverOpen = () => + wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); + + expect(isSavedQueryPopoverOpen()).toBeFalsy(); + + wrapper + .find('button[data-test-subj="saved-query-management-popover-button"]') + .simulate('click'); + + expect(isSavedQueryPopoverOpen()).toBeTruthy(); + + wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click'); + + expect(isSavedQueryPopoverOpen()).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 77d4d4364acdd..23ac6cc5b813d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -95,13 +95,6 @@ export const MalwareProtections = React.memo(() => { }), protection: 'malware', }, - { - id: ProtectionModes.preventNotify, - label: i18n.translate('xpack.securitySolution.endpoint.policy.details.preventAndNotify', { - defaultMessage: 'Prevent and notify user', - }), - protection: 'malware', - }, ]; }, []); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts index ef7ee26cd3ecf..5af2f3ef488b0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts @@ -69,6 +69,6 @@ export const COLLAPSE = i18n.translate( export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( 'xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip', { - defaultMessage: 'Investigate in Resolver', + defaultMessage: 'Analyze event', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index b27f213c6a02c..d1a58dafcb328 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -204,6 +204,7 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__tdGroupActions ${className}`, }))<{ actionsColumnWidth: number }>` + align-items: center; display: flex; flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`}; min-width: 0; diff --git a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js index 344d0f0e5131a..5c31b3fad685a 100644 --- a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js @@ -9,6 +9,7 @@ require('../../../../src/setup_node_env'); const fs = require('fs'); // eslint-disable-next-line import/no-extraneous-dependencies const fetch = require('node-fetch'); +// eslint-disable-next-line import/no-extraneous-dependencies const { camelCase } = require('lodash'); const { resolve } = require('path'); diff --git a/x-pack/plugins/snapshot_restore/README.md b/x-pack/plugins/snapshot_restore/README.md new file mode 100644 index 0000000000000..e11483785e958 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/README.md @@ -0,0 +1,78 @@ +# Snapshot Restore + +## Quick steps for testing + +### File system + +1. Add the file system path you want to use to elasticsearch.yml or as part of starting up ES. Note that this path should point to a directory that exists. + +``` +path: + repo: /tmp/es-backups +``` + +or + +``` +yarn es snapshot --license=trial -E path.repo=/tmp/es-backups +``` + +2. Use Console or UI to add a repository. Use the file system path above as the `location` setting: + +``` +PUT /_snapshot/my_backup +{ + "type": "fs", + "settings": { + "location": "/tmp/es-backups", + "chunk_size": "10mb" + } +} +``` + +3. Adjust `settings` as necessary, all available settings can be found in docs: +https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html#_shared_file_system_repository + +### Readonly + +Readonly repositories only take `url` setting. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html#_read_only_url_repository + +It's easy to set up a `file:` url: +``` +PUT _snapshot/my_readonly_repository +{ + "type": "url", + "settings": { + "url": "file:///tmp/es-backups" + } +} +``` + +### Source only + +Source only repositories are special in that they are basically a wrapper around another repository type. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html#_source_only_repository + +This means that the settings that are available depends on the `delegate_type` parameter. For example, this source only repository delegates to `fs` (file system) type, so all file system rules and available settings apply: + +``` +PUT _snapshot/my_src_only_repository +{ + "type" : "source", + "settings" : { + "delegate_type" : "fs", + "location" : "/tmp/es-backups" + } +} +``` + +### Plugin-based repositories: + +There are four official repository plugins available: S3, GCS, HDFS, Azure. Available plugin repository settings can be found in the docs: https://www.elastic.co/guide/en/elasticsearch/plugins/master/repository.html. + +To run ES with plugins: + +1. Run `yarn es snapshot` from the Kibana directory like normal, then exit out of process. +2. `cd .es/8.0.0` +3. `bin/elasticsearch-plugin install https://snapshots.elastic.co/downloads/elasticsearch-plugins/repository-s3/repository-s3-8.0.0-SNAPSHOT.zip` +4. Repeat step 3 for additional plugins, replacing occurrences of `repository-s3` with the plugin you want to install. +5. Run `bin/elasticsearch` from the `.es/8.0.0` directory. Otherwise, starting ES with `yarn es snapshot` would overwrite the plugins you just installed. \ No newline at end of file diff --git a/x-pack/plugins/snapshot_restore/public/application/index.scss b/x-pack/plugins/snapshot_restore/public/application/index.scss index b680f4d3ebf90..3e16e3b5301e7 100644 --- a/x-pack/plugins/snapshot_restore/public/application/index.scss +++ b/x-pack/plugins/snapshot_restore/public/application/index.scss @@ -1,6 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - // Snapshot and Restore plugin styles // Prefix all styles with "snapshotRestore" to avoid conflicts. diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx index 739c72fe03a6f..3b18af7cebbf3 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx @@ -6,7 +6,7 @@ import React, { useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; import { EuiBasicTable, EuiButtonIcon, EuiHealth } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; @@ -58,7 +58,7 @@ export const RestoreTable: React.FunctionComponent = React.memo(({ restor } = getSorting(); const { pageIndex, pageSize } = getPagination(); - const sortedRestores = sortByOrder(newRestoresList, [field], [direction]); + const sortedRestores = orderBy(newRestoresList, [field], [direction]); return sortedRestores.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); }; 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 4c9f62503a21b..87c2fee4ea9bf 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 @@ -18,7 +18,7 @@ import { createLicensedRouteHandler } from '../../lib'; type SavedObjectIdentifier = Pick; const areObjectsUnique = (objects: SavedObjectIdentifier[]) => - _.uniq(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length; + _.uniqBy(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length; export function initCopyToSpacesApi(deps: ExternalRouteDeps) { const { externalRouter, spacesService, getImportExportObjectLimit, getStartServices } = deps; diff --git a/x-pack/plugins/task_manager/server/lib/get_template_version.ts b/x-pack/plugins/task_manager/server/lib/get_template_version.ts index eac9d09685a42..07a9076359f0c 100644 --- a/x-pack/plugins/task_manager/server/lib/get_template_version.ts +++ b/x-pack/plugins/task_manager/server/lib/get_template_version.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { padLeft } from 'lodash'; +import { padStart } from 'lodash'; /* * The logic for ID is: XXYYZZAA, where XX is major version, YY is minor @@ -27,7 +27,7 @@ export function getTemplateVersion(versionStr: string): number { const padded = splitted.map((v: string) => { const vMatches = v.match(/\d+/); if (vMatches) { - return padLeft(vMatches[0], 2, '0'); + return padStart(vMatches[0], 2, '0'); } return '00'; }); @@ -39,13 +39,13 @@ export function getTemplateVersion(versionStr: string): number { const matches = minorStr.match(/alpha(?\d+)/); if (matches != null && matches.groups != null) { const alphaVerInt = parseInt(matches.groups.alpha, 10); // alpha build indicator - buildV = padLeft(`${alphaVerInt}`, 2, '0'); + buildV = padStart(`${alphaVerInt}`, 2, '0'); } } else if (minorStr.match('beta')) { const matches = minorStr.match(/beta(?\d+)/); if (matches != null && matches.groups != null) { const alphaVerInt = parseInt(matches.groups.beta, 10) + 25; // beta build indicator - buildV = padLeft(`${alphaVerInt}`, 2, '0'); + buildV = padStart(`${alphaVerInt}`, 2, '0'); } } else { buildV = '99'; // release build indicator diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 17292adad3eb4..92374908c60f7 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -10,7 +10,7 @@ */ import moment, { Duration } from 'moment'; import { performance } from 'perf_hooks'; -import { padLeft } from 'lodash'; +import { padStart } from 'lodash'; import { Logger } from './types'; import { TaskRunner } from './task_runner'; import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; @@ -182,7 +182,7 @@ function partitionListByCount(list: T[], count: number): [T[], T[]] { function durationAsString(duration: Duration): string { const [m, s] = [duration.minutes(), duration.seconds()].map((value) => - padLeft(`${value}`, 2, '0') + padStart(`${value}`, 2, '0') ); return `${m}m ${s}s`; } diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts index 7a9fa0c45e15f..4c690a5675f61 100644 --- a/x-pack/plugins/task_manager/server/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_runner.ts @@ -359,7 +359,7 @@ export class TaskManagerRunner implements TaskRunner { await this.bufferedTaskStore.update( defaults( { - ...fieldUpdates, + ...(fieldUpdates as Partial), // reset fields that track the lifecycle of the concluded `task run` startedAt: null, retryAt: null, diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index fec72c317225b..771b4e2d7d9cb 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -853,7 +853,7 @@ if (doc['task.runAt'].size()!=0) { type, attributes: { ..._.omit(task, 'id'), - ..._.mapValues(_.pick(task, 'params', 'state'), (value) => JSON.stringify(value)), + ..._.mapValues(_.pick(task, ['params', 'state']), (value) => JSON.stringify(value)), }, references: [], version: '123', @@ -904,7 +904,7 @@ if (doc['task.runAt'].size()!=0) { type, attributes: { ..._.omit(task, 'id'), - ..._.mapValues(_.pick(task, 'params', 'state'), (value) => JSON.stringify(value)), + ..._.mapValues(_.pick(task, ['params', 'state']), (value) => JSON.stringify(value)), }, references: [], version: '123', diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index c63f4ac72ed20..4a691e17011e8 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -429,7 +429,7 @@ function taskInstanceToAttributes(doc: TaskInstance): SerializedConcreteTaskInst retryAt: (doc.retryAt && doc.retryAt.toISOString()) || null, runAt: (doc.runAt || new Date()).toISOString(), status: (doc as ConcreteTaskInstance).status || 'idle', - }; + } as SerializedConcreteTaskInstance; } export function savedObjectToConcreteTaskInstance( diff --git a/x-pack/plugins/transform/public/app/index.scss b/x-pack/plugins/transform/public/app/index.scss index beb5ee6be67e6..cc5cc87c754c9 100644 --- a/x-pack/plugins/transform/public/app/index.scss +++ b/x-pack/plugins/transform/public/app/index.scss @@ -1,6 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - // Transform plugin styles // Prefix all styles with "transform" to avoid conflicts. diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5c5d270d324ff..9a3f52a0ce47c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8469,13 +8469,7 @@ "xpack.ingestPipelines.form.nameFieldLabel": "名前", "xpack.ingestPipelines.form.nameTitle": "名前", "xpack.ingestPipelines.form.onFailureFieldHelpText": "JSONフォーマットを使用:{code}", - "xpack.ingestPipelines.form.onFailureFieldLabel": "障害プロセッサー(任意)", - "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "入力が無効です。", "xpack.ingestPipelines.form.pipelineNameRequiredError": "名前が必要です。", - "xpack.ingestPipelines.form.processorsFieldHelpText": "JSONフォーマットを使用:{code}", - "xpack.ingestPipelines.form.processorsFieldLabel": "プロセッサー", - "xpack.ingestPipelines.form.processorsJsonError": "入力が無効です。", - "xpack.ingestPipelines.form.processorsRequiredError": "プロセッサーが必要です。", "xpack.ingestPipelines.form.saveButtonLabel": "パイプラインを保存", "xpack.ingestPipelines.form.savePipelineError": "パイプラインを作成できません", "xpack.ingestPipelines.form.savingButtonLabel": "保存中…", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c71215d2bfb74..7aebb59e0f8a2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8473,13 +8473,7 @@ "xpack.ingestPipelines.form.nameFieldLabel": "名称", "xpack.ingestPipelines.form.nameTitle": "名称", "xpack.ingestPipelines.form.onFailureFieldHelpText": "使用 JSON 格式:{code}", - "xpack.ingestPipelines.form.onFailureFieldLabel": "失败处理器(可选)", - "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "输入无效。", "xpack.ingestPipelines.form.pipelineNameRequiredError": "“名称”必填。", - "xpack.ingestPipelines.form.processorsFieldHelpText": "使用 JSON 格式:{code}", - "xpack.ingestPipelines.form.processorsFieldLabel": "处理器", - "xpack.ingestPipelines.form.processorsJsonError": "输入无效。", - "xpack.ingestPipelines.form.processorsRequiredError": "需要指定处理器。", "xpack.ingestPipelines.form.saveButtonLabel": "保存管道", "xpack.ingestPipelines.form.savePipelineError": "无法创建管道", "xpack.ingestPipelines.form.savingButtonLabel": "正在保存......", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 957c79a5c5123..655f64995d147 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -47,6 +47,7 @@ export const AddMessageVariables: React.FunctionComponent = ({ setIsVariablesPopoverOpen(true)} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx index 053541d84c431..08616b2895a80 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx @@ -28,7 +28,7 @@ describe('EmailParamsFields renders', () => { expect( wrapper.find('[data-test-subj="toEmailAddressInput"]').first().prop('selectedOptions') ).toStrictEqual([{ label: 'test@test.com' }]); - expect(wrapper.find('[data-test-subj="emailSubjectInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailMessageInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="subjectInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx index 58a9685fc73d9..39c59a10fbc81 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -5,11 +5,12 @@ */ import React, { Fragment, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFieldText, EuiComboBox, EuiTextArea, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { EuiComboBox, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionParamsProps } from '../../../../types'; import { EmailActionParams } from '../types'; -import { AddMessageVariables } from '../../add_message_variables'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; export const EmailParamsFields = ({ actionParams, @@ -33,14 +34,6 @@ export const EmailParamsFields = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction( - paramsProperty, - ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), - index - ); - }; - return ( - onSelectMessageVariable('subject', variable) - } - paramsProperty="subject" - /> - } > - 0 && subject !== undefined} - name="subject" - data-test-subj="emailSubjectInput" - value={subject || ''} - onChange={(e) => { - editAction('subject', e.target.value, index); - }} - onBlur={() => { - if (!subject) { - editAction('subject', '', index); - } - }} + - 0 && message !== undefined} + - onSelectMessageVariable('message', variable) - } - paramsProperty="message" - /> - } - > - 0 && message !== undefined} - value={message || ''} - name="message" - data-test-subj="emailMessageInput" - onChange={(e) => { - editAction('message', e.target.value, index); - }} - onBlur={() => { - if (!message) { - editAction('message', '', index); - } - }} - /> - + errors={errors.message as string[]} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 1f8bfde2cd22b..4a6c13bf7f1a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -20,7 +20,7 @@ describe('IndexParamsFields renders', () => { index={0} /> ); - expect(wrapper.find('[data-test-subj="actionIndexDoc"]').first().prop('value')).toBe(`{ + expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(`{ "test": 123 }`); expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index 0b095cdc26984..fd6a3d64bd4be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -3,13 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { useXJsonMode } from '../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; import { ActionParamsProps } from '../../../../types'; import { IndexActionParams } from '.././types'; -import { AddMessageVariables } from '../../add_message_variables'; +import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; export const IndexParamsFields = ({ actionParams, @@ -18,62 +16,36 @@ export const IndexParamsFields = ({ messageVariables, }: ActionParamsProps) => { const { documents } = actionParams; - const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode( - documents && documents.length > 0 ? documents[0] : null - ); - const onSelectMessageVariable = (variable: string) => { - const value = (xJson ?? '').concat(` {{${variable}}}`); - setXJson(value); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(value)); - }; - function onDocumentsChange(updatedDocuments: string) { + const onDocumentsChange = (updatedDocuments: string) => { try { const documentsJSON = JSON.parse(updatedDocuments); editAction('documents', [documentsJSON], index); // eslint-disable-next-line no-empty } catch (e) {} - } + }; + return ( - - onSelectMessageVariable(variable)} - paramsProperty="documents" - /> + 0 ? ((documents[0] as unknown) as string) : '' + } + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', + { + defaultMessage: 'Document to index', + } + )} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel', + { + defaultMessage: 'Code editor', } - > - { - setXJson(xjson); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(xjson)); - }} - /> - - + )} + onDocumentsChange={onDocumentsChange} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx index 94bea3c51760d..1b26b1157add9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -39,7 +39,7 @@ describe('PagerDutyParamsFields renders', () => { expect(wrapper.find('[data-test-subj="componentInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="summaryInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx index f0b131deb1490..c8ad5f5b7080e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionParamsProps } from '../../../../types'; import { PagerDutyActionParams } from '.././types'; -import { AddMessageVariables } from '../../add_message_variables'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; const PagerDutyParamsFields: React.FunctionComponent> = ({ actionParams, @@ -94,15 +94,6 @@ const PagerDutyParamsFields: React.FunctionComponent { - editAction( - paramsProperty, - ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), - index - ); - }; - return ( @@ -159,29 +150,13 @@ const PagerDutyParamsFields: React.FunctionComponent - onSelectMessageVariable('dedupKey', variable) - } - paramsProperty="dedupKey" - /> - } > - ) => { - editAction('dedupKey', e.target.value, index); - }} - onBlur={() => { - if (!dedupKey) { - editAction('dedupKey', '', index); - } - }} + @@ -196,32 +171,14 @@ const PagerDutyParamsFields: React.FunctionComponent - onSelectMessageVariable('timestamp', variable) - } - paramsProperty="timestamp" - /> - } > - 0 && timestamp !== undefined} - onChange={(e: React.ChangeEvent) => { - editAction('timestamp', e.target.value, index); - }} - onBlur={() => { - if (timestamp?.trim()) { - editAction('timestamp', timestamp.trim(), index); - } else { - editAction('timestamp', '', index); - } - }} + @@ -234,29 +191,13 @@ const PagerDutyParamsFields: React.FunctionComponent - onSelectMessageVariable('component', variable) - } - paramsProperty="component" - /> - } > - ) => { - editAction('component', e.target.value, index); - }} - onBlur={() => { - if (!component) { - editAction('component', '', index); - } - }} + onSelectMessageVariable('group', variable)} - paramsProperty="group" - /> - } > - ) => { - editAction('group', e.target.value, index); - }} - onBlur={() => { - if (!group) { - editAction('group', '', index); - } - }} + onSelectMessageVariable('source', variable)} - paramsProperty="source" - /> - } > - ) => { - editAction('source', e.target.value, index); - }} - onBlur={() => { - if (!source) { - editAction('source', '', index); - } - }} + - onSelectMessageVariable('summary', variable) - } - paramsProperty="summary" - /> - } > - 0 && summary !== undefined} - name="summary" - value={summary || ''} - data-test-subj="pagerdutySummaryInput" - onChange={(e: React.ChangeEvent) => { - editAction('summary', e.target.value, index); - }} - onBlur={() => { - if (!summary) { - editAction('summary', '', index); - } - }} + onSelectMessageVariable('class', variable)} - paramsProperty="class" - /> - } > - ) => { - editAction('class', e.target.value, index); - }} - onBlur={() => { - if (!actionParams.class) { - editAction('class', '', index); - } - }} + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx index cb905023cae44..1849a7ec9817a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx @@ -27,7 +27,7 @@ describe('ServerLogParamsFields renders', () => { expect( wrapper.find('[data-test-subj="loggingLevelSelect"]').first().prop('value') ).toStrictEqual('trace'); - expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); }); test('level param field is rendered with default value if not selected', () => { @@ -47,6 +47,6 @@ describe('ServerLogParamsFields renders', () => { expect( wrapper.find('[data-test-subj="loggingLevelSelect"]').first().prop('value') ).toStrictEqual('info'); - expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx index c19aec2a993db..b79fa0ea94050 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx @@ -5,10 +5,10 @@ */ import React, { Fragment, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSelect, EuiTextArea, EuiFormRow } from '@elastic/eui'; +import { EuiSelect, EuiFormRow } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; import { ServerLogActionParams } from '.././types'; -import { AddMessageVariables } from '../../add_message_variables'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; export const ServerLogParamsFields: React.FunctionComponent { - editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); - }; - return ( - 0 && message !== undefined} + - onSelectMessageVariable('message', variable) - } - paramsProperty="message" - /> - } - > - 0 && message !== undefined} - value={message || ''} - name="message" - data-test-subj="loggingMessageInput" - onChange={(e) => { - editAction('message', e.target.value, index); - }} - onBlur={() => { - if (!message) { - editAction('message', '', index); - } - }} - /> - + errors={errors.message as string[]} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx index 6efa8d64dafbe..8777c8f48e0f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx @@ -20,9 +20,9 @@ describe('SlackParamsFields renders', () => { index={0} /> ); - expect(wrapper.find('[data-test-subj="slackMessageTextArea"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="slackMessageTextArea"]').first().prop('value') - ).toStrictEqual('test message'); + expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="messageTextArea"]').first().prop('value')).toStrictEqual( + 'test message' + ); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx index 5789d37a6bcf6..80a2f9d7709cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect } from 'react'; -import { EuiTextArea, EuiFormRow } from '@elastic/eui'; +import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { ActionParamsProps } from '../../../../types'; import { SlackActionParams } from '../types'; -import { AddMessageVariables } from '../../add_message_variables'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; const SlackParamsFields: React.FunctionComponent> = ({ actionParams, @@ -26,50 +25,21 @@ const SlackParamsFields: React.FunctionComponent { - editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); - }; - return ( - - 0 && message !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel', - { - defaultMessage: 'Message', - } - )} - labelAppend={ - - onSelectMessageVariable('message', variable) - } - paramsProperty="message" - /> + - 0 && message !== undefined} - name="message" - value={message || ''} - data-test-subj="slackMessageTextArea" - onChange={(e) => { - editAction('message', e.target.value, index); - }} - onBlur={() => { - if (!message) { - editAction('message', '', index); - } - }} - /> - - + )} + errors={errors.message as string[]} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx index 665114bd86e9b..9e57d7ae608cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx @@ -20,10 +20,10 @@ describe('WebhookParamsFields renders', () => { index={0} /> ); - expect(wrapper.find('[data-test-subj="webhookBodyEditor"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="webhookBodyEditor"]').first().prop('value') - ).toStrictEqual('test message'); + expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').first().prop('value')).toStrictEqual( + 'test message' + ); expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx index 9e802b96e16be..1dfd9e3edc2c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { ActionParamsProps } from '../../../../types'; import { WebhookActionParams } from '../types'; -import { AddMessageVariables } from '../../add_message_variables'; +import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; const WebhookParamsFields: React.FunctionComponent> = ({ actionParams, @@ -18,49 +17,28 @@ const WebhookParamsFields: React.FunctionComponent { const { body } = actionParams; - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index); - }; return ( - - 0 && body !== undefined} - fullWidth - error={errors.body} - labelAppend={ - onSelectMessageVariable('body', variable)} - paramsProperty="body" - /> + - { - editAction('body', json, index); - }} - /> - - + )} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel', + { + defaultMessage: 'Code editor', + } + )} + errors={errors.body as string[]} + onDocumentsChange={(json: string) => { + editAction('body', json, index); + }} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx new file mode 100644 index 0000000000000..2aac389dce5ec --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.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, { useState } from 'react'; +import { EuiCodeEditor, EuiFormRow } from '@elastic/eui'; +import './add_message_variables.scss'; +import { useXJsonMode } from '../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; + +import { AddMessageVariables } from './add_message_variables'; + +interface Props { + messageVariables: string[] | undefined; + paramsProperty: string; + inputTargetValue: string; + label: string; + errors?: string[]; + areaLabel?: string; + onDocumentsChange: (data: string) => void; +} + +export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ + messageVariables, + paramsProperty, + inputTargetValue, + label, + errors, + areaLabel, + onDocumentsChange, +}) => { + const [cursorPosition, setCursorPosition] = useState(null); + + const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode(inputTargetValue ?? null); + + const onSelectMessageVariable = (variable: string) => { + const templatedVar = `{{${variable}}}`; + let newValue = ''; + if (cursorPosition) { + const cursor = cursorPosition.getCursor(); + cursorPosition.session.insert(cursor, templatedVar); + newValue = cursorPosition.session.getValue(); + } else { + newValue = templatedVar; + } + setXJson(newValue); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(newValue)); + }; + + const onClickWithMessageVariable = (_value: any) => { + setCursorPosition(_value); + }; + + return ( + 0 && inputTargetValue !== undefined} + label={label} + labelAppend={ + onSelectMessageVariable(variable)} + paramsProperty={paramsProperty} + /> + } + > + { + setXJson(xjson); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(xjson)); + }} + onCursorChange={(_value: any) => onClickWithMessageVariable(_value)} + /> + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx new file mode 100644 index 0000000000000..0b8a9349ad5fb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { EuiTextArea, EuiFormRow } from '@elastic/eui'; +import './add_message_variables.scss'; +import { AddMessageVariables } from './add_message_variables'; + +interface Props { + messageVariables: string[] | undefined; + paramsProperty: string; + index: number; + inputTargetValue?: string; + editAction: (property: string, value: any, index: number) => void; + label: string; + errors?: string[]; +} + +export const TextAreaWithMessageVariables: React.FunctionComponent = ({ + messageVariables, + paramsProperty, + index, + inputTargetValue, + editAction, + label, + errors, +}) => { + const [currentTextElement, setCurrentTextElement] = useState(null); + + const onSelectMessageVariable = (variable: string) => { + const templatedVar = `{{${variable}}}`; + const startPosition = currentTextElement?.selectionStart ?? 0; + const endPosition = currentTextElement?.selectionEnd ?? 0; + const newValue = + (inputTargetValue ?? '').substring(0, startPosition) + + templatedVar + + (inputTargetValue ?? '').substring(endPosition, (inputTargetValue ?? '').length); + editAction(paramsProperty, newValue, index); + }; + + const onChangeWithMessageVariable = (e: React.ChangeEvent) => { + editAction(paramsProperty, e.target.value, index); + }; + + return ( + 0 && inputTargetValue !== undefined} + label={label} + labelAppend={ + onSelectMessageVariable(variable)} + paramsProperty={paramsProperty} + /> + } + > + 0 && inputTargetValue !== undefined} + name={paramsProperty} + value={inputTargetValue} + data-test-subj={`${paramsProperty}TextArea`} + onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} + onFocus={(e: React.FocusEvent) => { + setCurrentTextElement(e.target); + }} + onBlur={() => { + if (!inputTargetValue) { + editAction(paramsProperty, '', index); + } + }} + /> + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx new file mode 100644 index 0000000000000..e280fd3f34e99 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { EuiFieldText } from '@elastic/eui'; +import './add_message_variables.scss'; +import { AddMessageVariables } from './add_message_variables'; + +interface Props { + messageVariables: string[] | undefined; + paramsProperty: string; + index: number; + inputTargetValue?: string; + editAction: (property: string, value: any, index: number) => void; + errors?: string[]; +} + +export const TextFieldWithMessageVariables: React.FunctionComponent = ({ + messageVariables, + paramsProperty, + index, + inputTargetValue, + editAction, + errors, +}) => { + const [currentTextElement, setCurrentTextElement] = useState(null); + + const onSelectMessageVariable = (variable: string) => { + const templatedVar = `{{${variable}}}`; + const startPosition = currentTextElement?.selectionStart ?? 0; + const endPosition = currentTextElement?.selectionEnd ?? 0; + const newValue = + (inputTargetValue ?? '').substring(0, startPosition) + + templatedVar + + (inputTargetValue ?? '').substring(endPosition, (inputTargetValue ?? '').length); + editAction(paramsProperty, newValue, index); + }; + + const onChangeWithMessageVariable = (e: React.ChangeEvent) => { + editAction(paramsProperty, e.target.value, index); + }; + + return ( + 0 && inputTargetValue !== undefined} + data-test-subj={`${paramsProperty}Input`} + value={inputTargetValue} + onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} + onFocus={(e: React.FocusEvent) => { + setCurrentTextElement(e.target); + }} + onBlur={(e: React.FocusEvent) => { + if (!inputTargetValue) { + editAction(paramsProperty, '', index); + } + }} + append={ + onSelectMessageVariable(variable)} + paramsProperty={paramsProperty} + /> + } + /> + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx index 463d170df2296..838b684cc10e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCard, EuiLink } from '@elastic/eui'; @@ -30,7 +30,7 @@ const getLicenseCheckResult = (actionType: ActionType) => { { defaultMessage: 'This connector requires a {minimumLicenseRequired} license.', values: { - minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), + minimumLicenseRequired: upperFirst(actionType.minimumLicenseRequired), }, } ), @@ -42,7 +42,7 @@ const getLicenseCheckResult = (actionType: ActionType) => { { defaultMessage: 'This feature requires a {minimumLicenseRequired} license.', values: { - minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), + minimumLicenseRequired: upperFirst(actionType.minimumLicenseRequired), }, } )} 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 3a25417f7db4c..66a7ac25d4a70 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 @@ -5,7 +5,7 @@ */ import React, { useState, Fragment } from 'react'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; import { useHistory } from 'react-router-dom'; import { EuiPageBody, @@ -72,7 +72,7 @@ export const AlertDetails: React.FunctionComponent = ({ } = useAppDependencies(); const canSave = hasSaveAlertsCapability(capabilities); - const actionTypesByTypeId = indexBy(actionTypes, 'id'); + const actionTypesByTypeId = keyBy(actionTypes, 'id'); const hasEditButton = canSave && alertTypeRegistry.has(alert.alertTypeId) ? !alertTypeRegistry.get(alert.alertTypeId).requiresAppContext diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 9deeeb96124c8..799886d264547 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch } from '@elastic/eui'; // @ts-ignore import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; -import { padLeft, difference, chunk } from 'lodash'; +import { padStart, difference, chunk } from 'lodash'; import { Alert, AlertTaskState, RawAlertInstance, Pagination } from '../../../../types'; import { ComponentOpts as AlertApis, @@ -103,7 +103,7 @@ export const alertInstancesTableColumns = ( function durationAsString(duration: Duration): string { return [duration.hours(), duration.minutes(), duration.seconds()] - .map((value) => padLeft(`${value}`, 2, '0')) + .map((value) => padStart(`${value}`, 2, '0')) .join(':'); } diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts index 0bec188f63640..da332aa326ccf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { uniq } from 'lodash'; import { HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { @@ -47,7 +48,7 @@ export const getIndexOptions = async ( }) as string[]; if (matchingIndices.length || matchingIndexPatterns.length) { - const matchingOptions = _.uniq([...matchingIndices, ...matchingIndexPatterns]); + const matchingOptions = uniq([...matchingIndices, ...matchingIndexPatterns]); options.push({ label: i18n.translate( diff --git a/x-pack/plugins/upgrade_assistant/public/application/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/_index.scss index 6000af5498cd6..841415620d691 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/_index.scss +++ b/x-pack/plugins/upgrade_assistant/public/application/_index.scss @@ -1,3 +1 @@ -@import 'src/legacy/ui/public/styles/_styling_constants'; - @import 'components/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx index aeb5801c9f6b5..b7eafb7bf5c88 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx @@ -14,7 +14,9 @@ export const LEVEL_MAP: { [level: string]: number } = { critical: 1, }; -export const REVERSE_LEVEL_MAP: { [idx: number]: DeprecationInfo['level'] } = invert(LEVEL_MAP); +export const REVERSE_LEVEL_MAP: { [idx: number]: DeprecationInfo['level'] } = invert( + LEVEL_MAP +) as any; export const COLOR_MAP: { [level: string]: IconColor } = { warning: 'default', diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx index 25cf400bcd0fd..1c587568fe61d 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { MonitorSummary } from '../../../../../../common/runtime_types'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { IntegrationGroup } from '../actions_popover/integration_group'; +import { IntegrationGroup, extractSummaryValues } from '../actions_popover/integration_group'; describe('IntegrationGroup', () => { let summary: MonitorSummary; @@ -38,4 +38,97 @@ describe('IntegrationGroup', () => { const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); + + describe('extractSummaryValues', () => { + let mockSummary: Pick; + + beforeEach(() => { + mockSummary = { + state: { + timestamp: 'foo', + url: {}, + }, + }; + }); + + it('provides defaults when values are not present', () => { + expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` + Object { + "containerId": undefined, + "domain": "", + "ip": undefined, + "podUid": undefined, + } + `); + }); + + it('finds url domain', () => { + mockSummary.state.url.domain = 'mydomain'; + + expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` + Object { + "containerId": undefined, + "domain": "mydomain", + "ip": undefined, + "podUid": undefined, + } + `); + }); + + it('finds pod uid', () => { + mockSummary.state.checks = [ + { kubernetes: { pod: { uid: 'myuid' } }, monitor: { status: 'up' }, timestamp: 123 }, + ]; + + expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` + Object { + "containerId": undefined, + "domain": "", + "ip": undefined, + "podUid": "myuid", + } + `); + }); + + it('does not throw for missing kubernetes fields', () => { + mockSummary.state.checks = []; + + expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` + Object { + "containerId": undefined, + "domain": "", + "ip": undefined, + "podUid": undefined, + } + `); + }); + + it('finds container id', () => { + mockSummary.state.checks = [ + { container: { id: 'mycontainer' }, monitor: { status: 'up' }, timestamp: 123 }, + ]; + + expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` + Object { + "containerId": "mycontainer", + "domain": "", + "ip": undefined, + "podUid": undefined, + } + `); + }); + + it('finds ip field', () => { + mockSummary.state.checks = [{ monitor: { ip: '127.0.0.1', status: 'up' }, timestamp: 123 }]; + + expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` + Object { + "containerId": undefined, + "domain": "", + "ip": "127.0.0.1", + "podUid": undefined, + } + `); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx index bbcba7238748d..55a99ab8541f8 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { IntegrationLink } from './integration_link'; import { @@ -26,6 +25,20 @@ interface IntegrationGroupProps { summary: MonitorSummary; } +export const extractSummaryValues = (summary: Pick) => { + const domain = summary.state.url?.domain ?? ''; + const podUid = summary.state.checks?.[0]?.kubernetes?.pod.uid ?? undefined; + const containerId = summary.state.checks?.[0]?.container?.id ?? undefined; + const ip = summary.state.checks?.[0]?.monitor.ip ?? undefined; + + return { + domain, + podUid, + containerId, + ip, + }; +}; + export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { const { basePath, @@ -36,10 +49,7 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { isLogsAvailable, } = useContext(UptimeSettingsContext); - const domain = get(summary, 'state.url.domain', ''); - const podUid = get(summary, 'state.checks[0].kubernetes.pod.uid', undefined); - const containerId = get(summary, 'state.checks[0].container.id', undefined); - const ip = get(summary, 'state.checks[0].monitor.ip', undefined); + const { domain, podUid, containerId, ip } = extractSummaryValues(summary); return isApmAvailable || isInfraAvailable || isLogsAvailable ? ( @@ -97,7 +107,7 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { { defaultMessage: 'Check Infrastructure UI for the IP "{ip}"', values: { - ip, + ip: Array.isArray(ip) ? ip[0] : ip, }, } )} @@ -184,7 +194,12 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { )} tooltipContent={i18n.translate( 'xpack.uptime.monitorList.loggingIntegrationAction.ip.tooltip', - { defaultMessage: 'Check Logging UI for the IP "{ip}"', values: { ip } } + { + defaultMessage: 'Check Logging UI for the IP "{ip}"', + values: { + ip: Array.isArray(ip) ? ip[0] : ip, + }, + } )} /> diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx index 12fff376a1a3c..26d0093446461 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { get, capitalize } from 'lodash'; +import { get, upperFirst } from 'lodash'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { LocationLink } from '../../../common/location_link'; @@ -26,12 +26,12 @@ export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { checks.forEach((check: Check) => { // Doing this way because name is either string or null, get() default value only works on undefined value - const location = get(check, 'observer.geo.name', null) || UNNAMED_LOCATION; + const location = get(check, 'observer.geo.name', null) || UNNAMED_LOCATION; if (check.monitor.status === STATUS.UP) { - upChecks.add(capitalize(location)); + upChecks.add(upperFirst(location)); } else if (check.monitor.status === STATUS.DOWN) { - downChecks.add(capitalize(location)); + downChecks.add(upperFirst(location)); } }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx index a9d04e68b8929..f80c73dcf5bb0 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx @@ -7,7 +7,7 @@ import React from 'react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import styled from 'styled-components'; import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { parseTimestamp } from './parse_timestamp'; @@ -83,9 +83,9 @@ export const getLocationStatus = (checks: Check[], status: string) => { const location = check?.observer?.geo?.name ?? UNNAMED_LOCATION; if (check.monitor.status === STATUS.UP) { - upChecks.add(capitalize(location)); + upChecks.add(upperFirst(location)); } else if (check.monitor.status === STATUS.DOWN) { - downChecks.add(capitalize(location)); + downChecks.add(upperFirst(location)); } }); diff --git a/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index 3a940b4655b19..d6185f2c2589a 100644 --- a/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -7,7 +7,6 @@ import { CoreStart } from 'src/core/public'; import React from 'react'; import ReactDOM from 'react-dom'; -import { get } from 'lodash'; import { i18n as i18nFormatter } from '@kbn/i18n'; import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; import { getIntegratedAppAvailability } from './capabilities_adapter'; @@ -38,7 +37,7 @@ export const getKibanaFrameworkAdapter = ( INTEGRATED_SOLUTIONS ); - const canSave = get(capabilities, 'uptime.save', false); + const canSave = (capabilities.uptime.save ?? false) as boolean; const props: UptimeAppProps = { basePath: basePath.get(), diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts index 414b37939b381..397d23a18332c 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts @@ -21,7 +21,7 @@ export const buildHref = ( getHref: (value: string | string[] | undefined) => string | undefined ): string | undefined => { const queryValue = checks - .map((check) => get(check, path, undefined)) + .map((check) => get(check, path, undefined)) .filter((value: string | undefined) => value !== undefined); if (queryValue.length === 0) { return getHref(undefined); diff --git a/x-pack/plugins/uptime/public/state/effects/index_status.ts b/x-pack/plugins/uptime/public/state/effects/index_status.ts index 793a671f5fed8..a4b85312849a2 100644 --- a/x-pack/plugins/uptime/public/state/effects/index_status.ts +++ b/x-pack/plugins/uptime/public/state/effects/index_status.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { takeLatest } from 'redux-saga/effects'; +import { takeLeading } from 'redux-saga/effects'; import { indexStatusAction } from '../actions'; import { fetchIndexStatus } from '../api'; import { fetchEffectFactory } from './fetch_effect'; export function* fetchIndexStatusEffect() { - yield takeLatest( + yield takeLeading( indexStatusAction.get, fetchEffectFactory(fetchIndexStatus, indexStatusAction.success, indexStatusAction.fail) ); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts new file mode 100644 index 0000000000000..dd7996b68c41f --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.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 { sortChecksBy } from '../enrich_monitor_groups'; + +describe('enrich monitor groups', () => { + describe('sortChecksBy', () => { + it('identifies lesser geo name', () => { + expect( + sortChecksBy( + { observer: { geo: { name: 'less' } }, monitor: { status: 'up' } }, + { observer: { geo: { name: 'more' } }, monitor: { status: 'up' } } + ) + ).toBe(-1); + }); + + it('identifies greater geo name', () => { + expect( + sortChecksBy( + { observer: { geo: { name: 'more' } }, monitor: { status: 'up' } }, + { observer: { geo: { name: 'less' } }, monitor: { status: 'up' } } + ) + ).toBe(1); + }); + + it('identifies equivalent geo name and sorts by lesser ip', () => { + expect( + sortChecksBy( + { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } }, + { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.2', status: 'up' } } + ) + ).toBe(-1); + }); + + it('identifies equivalent geo name and sorts by greater ip', () => { + expect( + sortChecksBy( + { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.2', status: 'up' } }, + { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } } + ) + ).toBe(1); + }); + + it('identifies equivalent geo name and sorts by equivalent ip', () => { + expect( + sortChecksBy( + { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } }, + { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } } + ) + ).toBe(0); + }); + + it('handles equivalent ip arrays', () => { + expect( + sortChecksBy( + { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } }, + { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } } + ) + ).toBe(0); + }); + + it('handles non-equal ip arrays', () => { + expect( + sortChecksBy( + { + observer: { geo: { name: 'same' } }, + monitor: { ip: ['127.0.0.2', '127.0.0.9'], status: 'up' }, + }, + { + observer: { geo: { name: 'same' } }, + monitor: { ip: ['127.0.0.3', '127.0.0.1'], status: 'up' }, + } + ) + ).toBe(1); + }); + + it('handles undefined observer fields', () => { + expect( + sortChecksBy( + { observer: undefined, monitor: { ip: ['127.0.0.1'], status: 'up' } }, + { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } } + ) + ).toBe(-1); + }); + + it('handles undefined ip fields', () => { + expect( + sortChecksBy( + { observer: { geo: { name: 'same' } }, monitor: { ip: undefined, status: 'up' } }, + { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } } + ) + ).toBe(-1); + }); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index 6e52b3a11f25f..f5c4c55a4e300 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; import { Check, @@ -245,17 +244,17 @@ export const enrichMonitorGroups: MonitorEnricher = async ( const items = await queryContext.search(params); - const monitorBuckets = get(items, 'aggregations.monitors.buckets', []); + const monitorBuckets = items?.aggregations?.monitors?.buckets ?? []; const monitorIds: string[] = []; const summaries: MonitorSummary[] = monitorBuckets.map((monitor: any) => { - const monitorId = get(monitor, 'key.monitor_id'); + const monitorId = monitor.key.monitor_id; monitorIds.push(monitorId); const state: any = monitor.state?.value; state.timestamp = state['@timestamp']; const { checks } = state; - if (checks) { - state.checks = sortBy(checks, checksSortBy); + if (Array.isArray(checks)) { + checks.sort(sortChecksBy); state.checks = state.checks.map((check: any) => ({ ...check, timestamp: check['@timestamp'], @@ -276,7 +275,11 @@ export const enrichMonitorGroups: MonitorEnricher = async ( histogram: histogramMap[summary.monitor_id], })); - const sortedResItems: any = sortBy(resItems, 'monitor_id'); + const sortedResItems: any = resItems.sort((a, b) => { + if (a.monitor_id === b.monitor_id) return 0; + return a.monitor_id > b.monitor_id ? 1 : -1; + }); + if (queryContext.pagination.sortOrder === SortOrder.DESC) { sortedResItems.reverse(); } @@ -378,8 +381,29 @@ const cursorDirectionToOrder = (cd: CursorDirection): 'asc' | 'desc' => { return CursorDirection[cd] === CursorDirection.AFTER ? 'asc' : 'desc'; }; -type SortChecks = (check: Check) => string[]; -const checksSortBy = (check: Check) => [ - get(check, 'observer.geo.name'), - get(check, 'monitor.ip'), -]; +const getStringValue = (value: string | Array | null | undefined): string => { + if (Array.isArray(value)) { + value.sort(); + return value[0] ?? ''; + } + return value ?? ''; +}; + +export const sortChecksBy = ( + a: Pick, + b: Pick +) => { + const nameA: string = a.observer?.geo?.name ?? ''; + const nameB: string = b.observer?.geo?.name ?? ''; + + if (nameA === nameB) { + const ipA = getStringValue(a.monitor.ip); + const ipB = getStringValue(b.monitor.ip); + + if (ipA === ipB) { + return 0; + } + return ipA > ipB ? 1 : -1; + } + return nameA > nameB ? 1 : -1; +}; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx index b82f631a30fe7..01702a033d585 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import { EuiButton, diff --git a/x-pack/plugins/watcher/server/models/execute_details/execute_details.js b/x-pack/plugins/watcher/server/models/execute_details/execute_details.js index 9dc162e06fba0..189da025e7142 100644 --- a/x-pack/plugins/watcher/server/models/execute_details/execute_details.js +++ b/x-pack/plugins/watcher/server/models/execute_details/execute_details.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { omit, isUndefined } from 'lodash'; +import { omitBy, isUndefined } from 'lodash'; export class ExecuteDetails { constructor(props) { @@ -22,14 +22,14 @@ export class ExecuteDetails { }; const result = { - trigger_data: omit(triggerData, isUndefined), + trigger_data: omitBy(triggerData, isUndefined), ignore_condition: this.ignoreCondition, alternative_input: this.alternativeInput, action_modes: this.actionModes, record_execution: this.recordExecution, }; - return omit(result, isUndefined); + return omitBy(result, isUndefined); } // generate ExecuteDetails object from kibana response diff --git a/x-pack/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js b/x-pack/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js index 2bdd03e23c6dc..90cb65a77e9ae 100644 --- a/x-pack/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js +++ b/x-pack/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, contains, values } from 'lodash'; +import { get, includes, values } from 'lodash'; import { WATCH_TYPES } from '../../../../../common/constants'; export function getWatchType(watchJson) { const type = get(watchJson, 'metadata.xpack.type'); - if (contains(values(WATCH_TYPES), type)) { + if (includes(values(WATCH_TYPES), type)) { return type; } diff --git a/x-pack/test/api_integration/apis/fleet/agent_flow.ts b/x-pack/test/api_integration/apis/fleet/agent_flow.ts index 71057b81d1b09..a6a4003a554fc 100644 --- a/x-pack/test/api_integration/apis/fleet/agent_flow.ts +++ b/x-pack/test/api_integration/apis/fleet/agent_flow.ts @@ -18,7 +18,7 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); const esClient = getService('es'); - describe('fleet_agent_flow', () => { + describe.skip('fleet_agent_flow', () => { before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js index 292aabad85054..a563b956df344 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js @@ -7,50 +7,63 @@ import { API_BASE_PATH, INDEX_PATTERNS } from './constants'; export const registerHelpers = ({ supertest }) => { + let templatesCreated = []; + const getAllTemplates = () => supertest.get(`${API_BASE_PATH}/index_templates`); - const getOneTemplate = (name, isLegacy = true) => + const getOneTemplate = (name, isLegacy = false) => supertest.get(`${API_BASE_PATH}/index_templates/${name}?legacy=${isLegacy}`); - const getTemplatePayload = (name, isLegacy = true) => ({ - name, - order: 1, - indexPatterns: INDEX_PATTERNS, - version: 1, - template: { - settings: { - number_of_shards: 1, - index: { - lifecycle: { - name: 'my_policy', + const getTemplatePayload = (name, indexPatterns = INDEX_PATTERNS, isLegacy = false) => { + const baseTemplate = { + name, + indexPatterns, + version: 1, + template: { + settings: { + number_of_shards: 1, + index: { + lifecycle: { + name: 'my_policy', + }, }, }, - }, - mappings: { - _source: { - enabled: false, - }, - properties: { - host_name: { - type: 'keyword', + mappings: { + _source: { + enabled: false, }, - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, }, }, + aliases: { + alias1: {}, + }, }, - aliases: { - alias1: {}, + _kbnMeta: { + isLegacy, }, - }, - _kbnMeta: { - isLegacy, - }, - }); + }; + + if (isLegacy) { + baseTemplate.order = 1; + } else { + baseTemplate.priority = 1; + } - const createTemplate = (payload) => - supertest.post(`${API_BASE_PATH}/index_templates`).set('kbn-xsrf', 'xxx').send(payload); + return baseTemplate; + }; + + const createTemplate = (template) => { + templatesCreated.push({ name: template.name, isLegacy: template._kbnMeta.isLegacy }); + return supertest.post(`${API_BASE_PATH}/index_templates`).set('kbn-xsrf', 'xxx').send(template); + }; const deleteTemplates = (templates) => supertest @@ -64,6 +77,16 @@ export const registerHelpers = ({ supertest }) => { .set('kbn-xsrf', 'xxx') .send(payload); + // Delete all templates created during tests + const cleanUpTemplates = async () => { + try { + await deleteTemplates(templatesCreated); + templatesCreated = []; + } catch (e) { + // Silently swallow errors + } + }; + return { getAllTemplates, getOneTemplate, @@ -71,5 +94,6 @@ export const registerHelpers = ({ supertest }) => { createTemplate, updateTemplate, deleteTemplates, + cleanUpTemplates, }; }; diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index fcee8ed6a183f..3a3d73ab68412 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -22,33 +22,78 @@ export default function ({ getService }) { getTemplatePayload, deleteTemplates, updateTemplate, + cleanUpTemplates, } = registerHelpers({ supertest }); describe('index templates', () => { - after(() => Promise.all([cleanUpEsResources()])); + after(() => Promise.all([cleanUpEsResources(), cleanUpTemplates()])); describe('get all', () => { const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName); + const indexTemplate = getTemplatePayload(templateName, [getRandomString()]); + const legacyTemplate = getTemplatePayload(templateName, [getRandomString()], true); beforeEach(async () => { - await createTemplate(payload).expect(200); + const res1 = await createTemplate(indexTemplate); + if (res1.status !== 200) { + throw new Error(res1.body.message); + } + + const res2 = await createTemplate(legacyTemplate); + if (res2.status !== 200) { + throw new Error(res2.body.message); + } }); - // TODO: When the "Create" API handler is added for V2 template, - // update this test to list composable templates. it('should list all the index templates with the expected parameters', async () => { const { body: allTemplates } = await getAllTemplates().expect(200); - // Composable index templates may have been created by other apps, e.g. Ingest Manager, - // so we don't make any assertion about these contents. - expect(allTemplates.templates).to.be.an('array'); - - // Legacy templates - const legacyTemplate = allTemplates.legacyTemplates.find( - (template) => template.name === payload.name + // Index templates (composable) + const indexTemplateFound = allTemplates.templates.find( + (template) => template.name === indexTemplate.name ); + + if (!indexTemplateFound) { + throw new Error( + `Index template "${indexTemplate.name}" not found in ${JSON.stringify( + allTemplates.templates, + null, + 2 + )}` + ); + } + const expectedKeys = [ + 'name', + 'indexPatterns', + 'hasSettings', + 'hasAliases', + 'hasMappings', + 'ilmPolicy', + 'priority', + 'composedOf', + 'version', + '_kbnMeta', + ].sort(); + + expect(Object.keys(indexTemplateFound).sort()).to.eql(expectedKeys); + + // Legacy index templates + const legacyTemplateFound = allTemplates.legacyTemplates.find( + (template) => template.name === legacyTemplate.name + ); + + if (!legacyTemplateFound) { + throw new Error( + `Legacy template "${legacyTemplate.name}" not found in ${JSON.stringify( + allTemplates.legacyTemplates, + null, + 2 + )}` + ); + } + + const expectedLegacyKeys = [ 'name', 'indexPatterns', 'hasSettings', @@ -60,20 +105,40 @@ export default function ({ getService }) { '_kbnMeta', ].sort(); - expect(Object.keys(legacyTemplate).sort()).to.eql(expectedKeys); + expect(Object.keys(legacyTemplateFound).sort()).to.eql(expectedLegacyKeys); }); }); describe('get one', () => { const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName); - beforeEach(async () => { - await createTemplate(payload).expect(200); - }); + it('should return an index template with the expected parameters', async () => { + const template = getTemplatePayload(templateName, [getRandomString()]); + await createTemplate(template).expect(200); - it('should return the index template with the expected parameters', async () => { const { body } = await getOneTemplate(templateName).expect(200); + const expectedKeys = [ + 'name', + 'indexPatterns', + 'template', + 'composedOf', + 'ilmPolicy', + 'priority', + 'version', + '_kbnMeta', + ].sort(); + const expectedTemplateKeys = ['aliases', 'mappings', 'settings'].sort(); + + expect(body.name).to.equal(templateName); + expect(Object.keys(body).sort()).to.eql(expectedKeys); + expect(Object.keys(body.template).sort()).to.eql(expectedTemplateKeys); + }); + + it('should return a legacy index template with the expected parameters', async () => { + const legacyTemplate = getTemplatePayload(templateName, [getRandomString()], true); + await createTemplate(legacyTemplate).expect(200); + + const { body } = await getOneTemplate(templateName, true).expect(200); const expectedKeys = [ 'name', 'indexPatterns', @@ -94,14 +159,21 @@ export default function ({ getService }) { describe('create', () => { it('should create an index template', async () => { const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName); + const payload = getTemplatePayload(templateName, [getRandomString()]); + + await createTemplate(payload).expect(200); + }); + + it('should create a legacy index template', async () => { + const templateName = `template-${getRandomString()}`; + const payload = getTemplatePayload(templateName, [getRandomString()], true); await createTemplate(payload).expect(200); }); it('should throw a 409 conflict when trying to create 2 templates with the same name', async () => { const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName); + const payload = getTemplatePayload(templateName, [getRandomString()], true); await createTemplate(payload); @@ -110,7 +182,7 @@ export default function ({ getService }) { it('should validate the request payload', async () => { const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName); + const payload = getTemplatePayload(templateName, [getRandomString()], true); delete payload.indexPatterns; // index patterns are required @@ -124,13 +196,40 @@ export default function ({ getService }) { describe('update', () => { it('should update an index template', async () => { const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName); + const indexTemplate = getTemplatePayload(templateName, [getRandomString()]); - await createTemplate(payload).expect(200); + await createTemplate(indexTemplate).expect(200); + + let catTemplateResponse = await catTemplate(templateName); + + const { name, version } = indexTemplate; + + expect( + catTemplateResponse.find(({ name: templateName }) => templateName === name).version + ).to.equal(version.toString()); + + // Update template with new version + const updatedVersion = 2; + await updateTemplate({ ...indexTemplate, version: updatedVersion }, templateName).expect( + 200 + ); + + catTemplateResponse = await catTemplate(templateName); + + expect( + catTemplateResponse.find(({ name: templateName }) => templateName === name).version + ).to.equal(updatedVersion.toString()); + }); + + it('should update a legacy index template', async () => { + const templateName = `template-${getRandomString()}`; + const legacyIndexTemplate = getTemplatePayload(templateName, [getRandomString()], true); + + await createTemplate(legacyIndexTemplate).expect(200); let catTemplateResponse = await catTemplate(templateName); - const { name, version } = payload; + const { name, version } = legacyIndexTemplate; expect( catTemplateResponse.find(({ name: templateName }) => templateName === name).version @@ -138,7 +237,10 @@ export default function ({ getService }) { // Update template with new version const updatedVersion = 2; - await updateTemplate({ ...payload, version: updatedVersion }, templateName).expect(200); + await updateTemplate( + { ...legacyIndexTemplate, version: updatedVersion }, + templateName + ).expect(200); catTemplateResponse = await catTemplate(templateName); @@ -151,7 +253,7 @@ export default function ({ getService }) { describe('delete', () => { it('should delete an index template', async () => { const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName); + const payload = getTemplatePayload(templateName, [getRandomString()], true); await createTemplate(payload).expect(200); diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics.ts index 673ee3c3f8475..23b0a96ecd401 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics.ts @@ -61,13 +61,13 @@ export default function ({ getService }: FtrProviderContext) { return; } expect(resp.metrics.length).to.equal(1); - const metric = first(resp.metrics); + const metric = first(resp.metrics) as any; expect(metric).to.have.property('id', 'hostCpuUsage'); expect(metric).to.have.property('series'); - const series = first(metric.series); + const series = first(metric.series) as any; expect(series).to.have.property('id', 'user'); expect(series).to.have.property('data'); - const datapoint = last(series.data); + const datapoint = last(series.data) as any; expect(datapoint).to.have.property('timestamp', 1547571720000); expect(datapoint).to.have.property('value', 0.0018333333333333333); }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts index 554b17d505c5e..d372496d2d1d9 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts @@ -49,7 +49,7 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(1); - const firstSeries = first(body.series); + const firstSeries = first(body.series) as any; expect(firstSeries).to.have.property('id', 'Everything'); expect(firstSeries.columns).to.eql([ { name: 'timestamp', type: 'date' }, @@ -89,7 +89,7 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(1); - const firstSeries = first(body.series); + const firstSeries = first(body.series) as any; expect(firstSeries).to.have.property('id', 'Everything'); expect(firstSeries.columns).to.eql([ { name: 'timestamp', type: 'date' }, @@ -120,7 +120,7 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(1); - const firstSeries = first(body.series); + const firstSeries = first(body.series) as any; expect(firstSeries).to.have.property('id', 'Everything'); expect(firstSeries.columns).to.eql([]); expect(firstSeries.rows).to.have.length(0); @@ -151,7 +151,7 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(3); - const firstSeries = first(body.series); + const firstSeries = first(body.series) as any; expect(firstSeries).to.have.property('id', 'system.diskio'); expect(firstSeries.columns).to.eql([ { name: 'timestamp', type: 'date' }, @@ -196,7 +196,7 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(3); - const firstSeries = first(body.series); + const firstSeries = first(body.series) as any; expect(firstSeries).to.have.property('id', 'demo-stack-mysql-01 / eth0'); expect(firstSeries.columns).to.eql([ { name: 'timestamp', type: 'date' }, diff --git a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts index 1f4da602b108a..bb0934b73a4c7 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts @@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(5); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); expect(first(firstNode.path)).to.have.property( @@ -105,7 +105,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(65); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); expect(first(firstNode.path)).to.have.property( @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(136); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); expect(first(firstNode.path)).to.have.property( @@ -176,7 +176,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(1); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01'); @@ -187,7 +187,7 @@ export default function ({ getService }: FtrProviderContext) { name: 'cpu', value: 0.0032, max: 0.0038333333333333336, - avg: 0.0027944444444444444, + avg: 0.002794444444444445, }, ]); } @@ -215,7 +215,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(1); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01'); @@ -250,7 +250,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(1); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01'); @@ -287,7 +287,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(1); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01'); @@ -322,7 +322,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(1); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(2); expect(first(firstNode.path)).to.have.property('value', 'virtualbox'); @@ -350,7 +350,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(1); - const firstNode = first(nodes); + const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(3); expect(first(firstNode.path)).to.have.property('value', 'vagrant'); @@ -378,7 +378,7 @@ export default function ({ getService }: FtrProviderContext) { if (snapshot) { const { nodes } = snapshot; expect(nodes.length).to.equal(2); - const firstNode = nodes[0]; + const firstNode = nodes[0] as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(2); expect(firstNode.path[0]).to.have.property('value', 'mysql'); @@ -389,10 +389,10 @@ export default function ({ getService }: FtrProviderContext) { name: 'cpu', value: 0.0032, max: 0.0038333333333333336, - avg: 0.0027944444444444444, + avg: 0.002794444444444445, }, ]); - const secondNode = nodes[1]; + const secondNode = nodes[1] as any; expect(secondNode).to.have.property('path'); expect(secondNode.path.length).to.equal(2); expect(secondNode.path[0]).to.have.property('value', 'system'); @@ -403,7 +403,7 @@ export default function ({ getService }: FtrProviderContext) { name: 'cpu', value: 0.0032, max: 0.0038333333333333336, - avg: 0.0027944444444444444, + avg: 0.002794444444444445, }, ]); } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index de0abe2350eb5..78915f6580299 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import util from 'util'; -import { isEqual } from 'lodash'; +import { isEqual, isEqualWith } from 'lodash'; import expect from '@kbn/expect/expect.js'; import { RawKibanaPrivileges } from '../../../../plugins/security/common/model'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -53,7 +53,7 @@ export default function ({ getService }: FtrProviderContext) { // supertest uses assert.deepStrictEqual. // expect.js doesn't help us here. // and lodash's isEqual doesn't know how to compare Sets. - const success = isEqual(res.body, expected, (value, other, key) => { + const success = isEqualWith(res.body, expected, (value, other, key) => { if (Array.isArray(value) && Array.isArray(other)) { if (key === 'reserved') { // order does not matter for the reserved privilege set. diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 00bfcdc119e47..d2bfdbe4dc967 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import util from 'util'; -import { isEqual } from 'lodash'; +import { isEqual, isEqualWith } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -51,7 +51,7 @@ export default function ({ getService }: FtrProviderContext) { // supertest uses assert.deepStrictEqual. // expect.js doesn't help us here. // and lodash's isEqual doesn't know how to compare Sets. - const success = isEqual(res.body, expected, (value, other, key) => { + const success = isEqualWith(res.body, expected, (value, other, key) => { if (Array.isArray(value) && Array.isArray(other)) { return isEqual(value.sort(), other.sort()); } diff --git a/x-pack/test/apm_api_integration/trial/tests/annotations.ts b/x-pack/test/apm_api_integration/trial/tests/annotations.ts index cd78f0ff7b88d..662879c495230 100644 --- a/x-pack/test/apm_api_integration/trial/tests/annotations.ts +++ b/x-pack/test/apm_api_integration/trial/tests/annotations.ts @@ -18,7 +18,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { function expectContainsObj(source: JsonObject, expected: JsonObject) { expect(source).to.eql( - merge(cloneDeep(source), expected, (a, b) => { + merge(cloneDeep(source), expected, (a: any, b: any) => { if (isPlainObject(a) && isPlainObject(b)) { return undefined; } diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index b980116c581da..9146ec7334625 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index b399c9e915e27..97cdd081705a4 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index c0a1ff34db96b..72f463be48fd5 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); @@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }) { global: ['all'], }, }); - const roles = indexBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); + const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); log.debug('actualRoles = %j', roles); expect(roles).to.have.key('myroleEast'); expect(roles.myroleEast.reserved).to.be(false); @@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: ['kibana_admin', 'myroleEast'], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.userEast.roles).to.eql(['kibana_admin', 'myroleEast']); expect(users.userEast.reserved).to.be(false); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index ec6b91219b7c4..7b22d72885c9d 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); @@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.common.sleep(1000); - const roles = indexBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); + const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); log.debug('actualRoles = %j', roles); expect(roles).to.have.key('a_viewssnrole'); expect(roles.a_viewssnrole.reserved).to.be(false); @@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }) { }, }); await PageObjects.common.sleep(1000); - const roles = indexBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); + const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); log.debug('actualRoles = %j', roles); expect(roles).to.have.key('a_view_no_ssn_role'); expect(roles.a_view_no_ssn_role.reserved).to.be(false); @@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: ['kibana_admin', 'a_viewssnrole'], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.customer1.roles).to.eql(['kibana_admin', 'a_viewssnrole']); }); @@ -97,7 +97,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: ['kibana_admin', 'a_view_no_ssn_role'], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.customer2.roles).to.eql(['kibana_admin', 'a_view_no_ssn_role']); }); diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index e9c09d1af6ea6..b138859d01361 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects([ 'security', @@ -70,7 +70,7 @@ export default function ({ getService, getPageObjects }) { roles: ['rbac_all'], }); log.debug('After Add user: , userObj.userName'); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); log.debug('roles: ', users.kibanauser.roles); expect(users.kibanauser.roles).to.eql(['rbac_all']); @@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }) { roles: ['rbac_read'], }); log.debug('After Add user: , userObj.userName'); - const users1 = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users1 = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); const user = users1.kibanareadonly; log.debug('actualUsers = %j', users1); log.debug('roles: ', user.roles); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index 94b4306819052..2054a7b0b0038 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects([ 'security', @@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }) { roles: ['logstash_reader', 'kibana_admin'], }); log.debug('After Add user: , userObj.userName'); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); log.debug('roles: ', users.Rashmi.roles); expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_admin']); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index b6e3a84d6f7c0..a2a2b705172d7 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['security', 'settings', 'common', 'accountSetting']); const log = getService('log'); @@ -28,7 +28,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: ['kibana_admin', 'superuser'], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.newuser.roles).to.eql(['kibana_admin', 'superuser']); expect(users.newuser.fullname).to.eql('newuserFirst newuserLast'); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 4c8c04c0356b0..4fd4384a93c59 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['security', 'settings']); const config = getService('config'); @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }) { }); it('should show the default elastic and kibana_system users', async function () { - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.info('actualUsers = %j', users); log.info('config = %j', config.get('servers.elasticsearch.hostname')); if (config.get('servers.elasticsearch.hostname') === 'localhost') { @@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: ['kibana_admin'], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.Lee.roles).to.eql(['kibana_admin']); expect(users.Lee.fullname).to.eql('LeeFirst LeeLast'); @@ -66,7 +66,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: [], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.OptionalUser.roles).to.eql(['']); expect(users.OptionalUser.fullname).to.eql(''); @@ -77,14 +77,14 @@ export default function ({ getService, getPageObjects }) { it('should delete user', async function () { const alertMsg = await PageObjects.security.deleteUser('Lee'); log.debug('alertMsg = %s', alertMsg); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users).to.not.have.key('Lee'); }); it('should show the default roles', async function () { await PageObjects.security.clickElasticsearchRoles(); - const roles = indexBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); + const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); log.debug('actualRoles = %j', roles); // This only contains the first page of alphabetically sorted results, so the assertions are only for the first handful of expected roles. expect(roles.apm_system.reserved).to.be(true); diff --git a/x-pack/test/functional/apps/watcher/watcher_test.js b/x-pack/test/functional/apps/watcher/watcher_test.js index 7a2eebc118ab3..1dd3fb6bbcc3d 100644 --- a/x-pack/test/functional/apps/watcher/watcher_test.js +++ b/x-pack/test/functional/apps/watcher/watcher_test.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; const watchID = 'watchID'; const watchName = 'watch Name'; @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }) { it('should delete the watch', async () => { // Navigate to the main list page await PageObjects.common.navigateToApp('watcher'); - const watchList = indexBy(await PageObjects.watcher.getWatches(), 'id'); + const watchList = keyBy(await PageObjects.watcher.getWatches(), 'id'); log.debug(watchList); expect(watchList.watchID.name).to.eql([watchName]); await PageObjects.watcher.deleteWatch(watchID); diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index c317aad8ba05b..b3d49199b0d9e 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -220,8 +220,7 @@ ], "revision": 2, "updated_at": "2020-05-07T19:34:42.533Z", - "updated_by": "system", - "id": "config1" + "updated_by": "system" } } } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 13bf47676cc09..2225316bba80f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -75,9 +75,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); const createdConnectorToastTitle = await pageObjects.common.closeToast(); expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); - await testSubjects.setValue('slackMessageTextArea', 'test message'); + await testSubjects.setValue('messageTextArea', 'test message '); await testSubjects.click('messageAddVariableButton'); await testSubjects.click('variableMenuButton-0'); + const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]'); + expect(await messageTextArea.getAttribute('value')).to.eql('test message {{alertId}}'); + await messageTextArea.type(' some additional text '); + + await testSubjects.click('messageAddVariableButton'); + await testSubjects.click('variableMenuButton-1'); + + expect(await messageTextArea.getAttribute('value')).to.eql( + 'test message {{alertId}} some additional text {{alertName}}' + ); await testSubjects.click('saveAlertButton'); const toastTitle = await pageObjects.common.closeToast(); 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 e22b098e6ee0c..d86d272c1da8c 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 @@ -375,7 +375,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const { alertInstances } = await alerting.alerts.getAlertState(alert.id); - const dateOnAllInstancesFromApiResponse = mapValues>( + const dateOnAllInstancesFromApiResponse = mapValues( alertInstances, ({ meta: { diff --git a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json index 35a420561731a..2b174a8cae076 100644 --- a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json +++ b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json @@ -11,7 +11,6 @@ "build": "rm -rf './target' && tsc" }, "dependencies": { - "lodash": "^4.17.15", "uuid": "3.3.2", "stats-lite": "2.2.0", "pretty-ms": "5.0.0" diff --git a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts index 9fbe9f26944ca..ba6d7ced3c591 100644 --- a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts +++ b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts @@ -265,8 +265,8 @@ function avg(items: number[]) { return { mean: Math.round(stats.mean(items)), range: { - min: Math.round(isNumericArray(mode) ? _.min([...mode]) : mode), - max: Math.round(isNumericArray(mode) ? _.max([...mode]) : mode), + min: Math.round(isNumericArray(mode) ? (_.min([...mode]) as number) : (mode as number)), + max: Math.round(isNumericArray(mode) ? (_.max([...mode]) as number) : (mode as number)), }, }; } 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 45ea82c59bf97..bacba619e5648 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 @@ -118,45 +118,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, policy: { linux: { - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { connect: true, process: true }, - }, - }, events: { file: false, network: true, process: true }, - logging: { file: 'info', stdout: 'debug' }, + logging: { file: 'info' }, }, mac: { - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { connect: true, process: true }, - }, - }, events: { file: false, network: true, process: true }, - logging: { file: 'info', stdout: 'debug' }, - malware: { mode: 'detect' }, + logging: { file: 'info' }, + malware: { mode: 'prevent' }, }, windows: { - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { connect: true, process: true }, - }, - }, events: { dll_and_driver_load: true, dns: true, @@ -166,7 +136,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { registry: true, security: true, }, - logging: { file: 'info', stdout: 'debug' }, + logging: { file: 'info' }, malware: { mode: 'prevent' }, }, }, diff --git a/x-pack/test_utils/router_helpers.tsx b/x-pack/test_utils/router_helpers.tsx index 76c1e2259545b..f2099e1eb7c91 100644 --- a/x-pack/test_utils/router_helpers.tsx +++ b/x-pack/test_utils/router_helpers.tsx @@ -16,9 +16,10 @@ export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex: ); -export const WithRoute = (componentRoutePath = '/', onRouter = (router: any) => {}) => ( - WrappedComponent: ComponentType -) => { +export const WithRoute = ( + componentRoutePath: string | string[] = '/', + onRouter = (router: any) => {} +) => (WrappedComponent: ComponentType) => { // Create a class component that will catch the router // and forward it to our "onRouter()" handler. const CatchRouter = withRouter( diff --git a/x-pack/test_utils/testbed/types.ts b/x-pack/test_utils/testbed/types.ts index 4975e073eea1f..e2b6693ce77aa 100644 --- a/x-pack/test_utils/testbed/types.ts +++ b/x-pack/test_utils/testbed/types.ts @@ -163,7 +163,7 @@ export interface MemoryRouterConfig { /** The React Router **initial index** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */ initialIndex?: number; /** The route **path** for the mounted component (defaults to `"/"`) */ - componentRoutePath?: string; + componentRoutePath?: string | string[]; /** A callBack that will be called with the React Router instance once mounted */ onRouter?: (router: any) => void; } diff --git a/x-pack/typings/index.d.ts b/x-pack/typings/index.d.ts index 1bf1370ad467f..73efee0dab2eb 100644 --- a/x-pack/typings/index.d.ts +++ b/x-pack/typings/index.d.ts @@ -22,11 +22,6 @@ declare module '*.svg' { export default content; } -declare module 'lodash/internal/toPath' { - function toPath(value: string | string[]): string[]; - export = toPath; -} - type MethodKeysOf = { [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; }[keyof T]; diff --git a/yarn.lock b/yarn.lock index ace6181797246..02f7e90ab7d24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5325,35 +5325,21 @@ "@types/node" "*" "@types/webpack" "*" -"@types/lodash.clonedeep@^4.5.4": - version "4.5.4" - resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.4.tgz#2515c5f08bc95afebfb597711871b0497f5d7da7" - integrity sha512-+rCVPIZOJaub++wU/lmyp/SxiKlqXQaXI5LryzjuHBKFj51ApVt38Xxk9psLWNGMuR/obEQNTH0l/yDfG4ANNQ== - dependencies: - "@types/lodash" "*" +"@types/lodash@4.14.149", "@types/lodash@^4.14.155": + version "4.14.156" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.156.tgz#cbe30909c89a1feeb7c60803e785344ea0ec82d1" + integrity sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ== -"@types/lodash.clonedeepwith@^4.5.3": - version "4.5.3" - resolved "https://registry.yarnpkg.com/@types/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.3.tgz#8057f074de743bdcff59fdbf26cd04c674a186cc" - integrity sha512-DNOO/Vec+yrzxxYwRXhVxTE4cOE1Xkf1xUzHhz3atoQ2URYKjvO5m9S7UxUcpn48rXkt9PxOT6cOyJCMIfjLNg== - dependencies: - "@types/lodash" "*" +"@types/lodash@^3.10.1": + version "3.10.3" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.3.tgz#aaddec6a3c93bf03b402db3acf5d4c77bce8bdff" + integrity sha512-b9zScBKmB/RJqETbxu3YRya61vJOik89/lR+NdxjZAFMDcMSjwX6IhQoP4terJkhsa9TE1C+l6XwxCkhhsaZXg== -"@types/lodash@*", "@types/lodash@^4.14.110", "@types/lodash@^4.14.116": +"@types/lodash@^4.14.116": version "4.14.150" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.150.tgz#649fe44684c3f1fcb6164d943c5a61977e8cf0bd" integrity sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w== -"@types/lodash@4.14.149": - version "4.14.149" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" - integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== - -"@types/lodash@^3.10.1": - version "3.10.2" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.2.tgz#c1fbda1562ef5603c8192fe1fe65b017849d5873" - integrity sha512-TmlYodTNhMzVzv3CK/9sXGzh31jWsRKHE3faczhVgYFCdXIRQRCOPD+0NDlR+SvJlCj914yP3q3aAupt53p2Ug== - "@types/log-symbols@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/log-symbols/-/log-symbols-2.0.0.tgz#7919e2ec3c8d13879bfdcab310dd7a3f7fc9466d" @@ -20672,16 +20658,6 @@ lodash.clone@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.clonedeepwith@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz#6ee30573a03a1a60d670a62ef33c10cf1afdbdd4" - integrity sha1-buMFc6A6GmDWcKYu8zwQzxr9vdQ= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -20792,26 +20768,16 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= -lodash.kebabcase@^4.0.0, lodash.kebabcase@^4.1.1: +lodash.kebabcase@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY= -lodash.keyby@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.keyby/-/lodash.keyby-4.6.0.tgz#7f6a1abda93fd24e22728a4d361ed8bcba5a4354" - integrity sha1-f2oavak/0k4icopNNh7YvLpaQ1Q= - lodash.map@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= -lodash.mean@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/lodash.mean/-/lodash.mean-4.1.0.tgz#bb985349628c0b9d7fe0f5fcc0011a2ee2c0dd7a" - integrity sha1-u5hTSWKMC51/4PX8wAEaLuLA3Xo= - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -20872,11 +20838,6 @@ lodash.set@^4.3.2: resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= -lodash.snakecase@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" - integrity sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40= - lodash.some@^4.4.0, lodash.some@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -20887,11 +20848,6 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash.startcase@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" - integrity sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg= - lodash.template@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" @@ -20912,11 +20868,6 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= -lodash.topath@^4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009" - integrity sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak= - lodash.unescape@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" @@ -20932,11 +20883,6 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash.uniqby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" - integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= - lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"