diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index bd55bd73966f..3986367d660a 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -23,15 +23,22 @@ kibanaPipeline(timeoutMinutes: 240) { } def handleIngestion(timestamp) { + def previousSha = handlePreviousSha() kibanaPipeline.downloadCoverageArtifacts() kibanaCoverage.prokLinks("### Process HTML Links") kibanaCoverage.collectVcsInfo("### Collect VCS Info") kibanaCoverage.generateReports("### Merge coverage reports") kibanaCoverage.uploadCombinedReports() - kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, '### Ingest && Upload') + kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, previousSha, '### Ingest && Upload') kibanaCoverage.uploadCoverageStaticSite(timestamp) } +def handlePreviousSha() { + def previous = kibanaCoverage.downloadPrevious('### Download OLD Previous') + kibanaCoverage.uploadPrevious('### Upload NEW Previous') + return previous +} + def handleFail() { def buildStatus = buildUtils.getBuildStatus() if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index ed642f22cfeb..97099c6f8744 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -14,6 +14,7 @@ pipeline { HOME = "${env.WORKSPACE}" E2E_DIR = 'x-pack/plugins/apm/e2e' PIPELINE_LOG_LEVEL = 'DEBUG' + KBN_OPTIMIZER_THEMES = 'v7light' } options { timeout(time: 1, unit: 'HOURS') diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index a3470cd75073..aafdf06433c6 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/.fossa.yml b/.fossa.yml new file mode 100755 index 000000000000..17d86d1f8552 --- /dev/null +++ b/.fossa.yml @@ -0,0 +1,15 @@ +# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli) +# Visit https://fossa.com to learn more + +version: 2 +cli: + server: https://app.fossa.com + fetcher: custom + project: kibana +analyze: + modules: + - name: kibana + type: nodejs + strategy: yarn.lock + target: . + path: . diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4cc0c8016f1d..754043ee0ef7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,7 +11,7 @@ Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server) -- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) +- [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7345f4b2897..a0aeed7a3494 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/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md new file mode 100644 index 000000000000..3f2d81cc97c7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md) + +## SavedObjectsComplexFieldMapping.doc\_values property + +Signature: + +```typescript +doc_values?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md index a7d13b0015e3..cb81686b424e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md @@ -18,6 +18,7 @@ export interface SavedObjectsComplexFieldMapping | Property | Type | Description | | --- | --- | --- | +| [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md) | boolean | | | [properties](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties | | | [type](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md new file mode 100644 index 000000000000..2a79eafd85a6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) > [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md) + +## SavedObjectsCoreFieldMapping.doc\_values property + +Signature: + +```typescript +doc_values?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md index 9a31d37b3ff3..b9e726eac799 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md @@ -16,6 +16,7 @@ export interface SavedObjectsCoreFieldMapping | Property | Type | Description | | --- | --- | --- | +| [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md) | boolean | | | [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean | | | [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
ignore_above?: number;
};
} | | | [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean | | 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 5fa7d4841537..48ec9456c56d 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/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index cd07596ad37e..8fc2b7381de8 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -59,6 +59,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn) * https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek) * https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity. +* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan) [float] === Other diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 928878fdcdb0..c83cd068eff5 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 b520be4df696..8e51f9207eaf 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", @@ -122,7 +123,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.10.1", "@babel/register": "^7.10.1", "@elastic/apm-rum": "^5.2.0", - "@elastic/charts": "19.5.2", + "@elastic/charts": "19.7.0", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", "@elastic/eui": "24.1.0", @@ -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 015dca128ce9..10b607dcd431 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 ea72a4a48cae..c6bb06e68b9c 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 25b122f40071..16572cf494cd 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 9ff0f5634427..5d5c5e3b6eb7 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 000000000000..63beb9927b9f --- /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 000000000000..4040cab1878f --- /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 000000000000..3918413c0686 --- /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 7d021a5ee784..89cde2c1cd06 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 000000000000..019a9b7bdee3 --- /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 000000000000..27b5e12b807a --- /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 a1ab51ee97c2..8726b3452ff1 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 b6b0973f0d53..1466865df8d9 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 @@ -1,68 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConfig 1`] = ` -OptimizerConfig { - "bundles": Array [ - Bundle { - "cache": BundleCache { - "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, - "state": undefined, - }, - "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, - "id": "bar", - "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, - "publicDirNames": Array [ - "public", - ], - "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, - "type": "plugin", - }, - Bundle { - "cache": BundleCache { - "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, - "state": undefined, - }, - "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, - "id": "foo", - "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, - "publicDirNames": Array [ - "public", - ], - "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, - "type": "plugin", - }, - ], - "cache": true, - "dist": false, - "inspectWorkers": false, - "maxWorkerCount": 1, - "plugins": Array [ - Object { - "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, - "extraPublicDirs": Array [], - "id": "bar", - "isUiPlugin": true, - }, - Object { - "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, - "extraPublicDirs": Array [], - "id": "foo", - "isUiPlugin": true, - }, - Object { - "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz, - "extraPublicDirs": Array [], - "id": "baz", - "isUiPlugin": false, - }, - ], - "profileWebpack": false, - "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, - "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 { await del(TMP_DIR); }); -it('builds expected bundles, saves bundle counts to metadata', async () => { +// FLAKY: https://github.com/elastic/kibana/issues/70762 +it.skip('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], @@ -167,7 +168,8 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { `); }); -it('uses cache on second run and exist cleanly', async () => { +// FLAKY: https://github.com/elastic/kibana/issues/70764 +it.skip('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], @@ -180,7 +182,7 @@ it('uses cache on second run and exist cleanly', async () => { 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 +228,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 cbec159bd27a..23767be610da 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 9d7f1709506f..47d01347a8f7 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/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts new file mode 100644 index 000000000000..9bf8f9db1fe4 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { 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 000000000000..e8daecef8e0d --- /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 fef3efc13a51..31b34bd5c593 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 d4152133f289..5b46d67479fd 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 c9e9b3ad01cc..7757004139d0 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 1572f459e6ee..09f8ca10c618 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 de5e9372e9e7..ca7673748bde 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 5d02462ef1bb..f2f685bde65d 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 11f5544cd927..aaea70d12c60 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 e61037e42d63..398b49fa1ecd 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 b9df67b32e5d..5c1e98cd869d 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 7fc29b1e6bd0..dc4d8a2fc10f 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 3e7ed49c6131..188db0a8321a 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 96ce6fd1d919..cf4ecbb4ad42 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 534f503e2956..740ee3819c36 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 caeffaabea62..b2df4f40d4fb 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 042de2617565..0c49ccf276b2 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 e38520f00e45..687a0e87d4c6 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 f795b32d78b8..2d4c461cc2c2 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 21dc681951b2..db53df789ac6 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 177fd1f15315..b7ba1e87b2f0 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 4da4fb21fbed..abf64906e025 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 02b64157686c..0f981f3d0761 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/package.json b/packages/kbn-ui-shared-deps/package.json index 0e3bb235c3d9..5f306cd5128b 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "19.5.2", + "@elastic/charts": "19.7.0", "@elastic/eui": "24.1.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/packages/kbn-ui-shared-deps/theme.ts b/packages/kbn-ui-shared-deps/theme.ts index ca4714779d39..4b2758516fc2 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 49a255d60f29..5a807b4b090c 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 6c10539c6b7d..effa6c77d761 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 66f68f815eda..2ddccae2fada 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 dc6e6d567665..097a54918742 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 656944d85b25..0170cb53e19d 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 bf9b4235e944..e31094d96f3d 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 4be46899cff6..87825350b4e9 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 cb8671ba37a6..7dc5f3655fca 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 cb279b2cc4c8..c4daaf5d7f30 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 d3ba506b865a..165ef98be91d 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 95296346ad83..06869089598a 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 483534e0c145..715f5b883139 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 f81903d76547..3b3b8da51a90 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 ffbdabadd03f..eccc9d013176 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 3b16bed92df9..4a6d86a0dfba 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 a31c9fe0c3ba..91b2b1239fc5 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/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index c037ed733549..7521e4a4bee8 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -133,6 +133,7 @@ export interface SavedObjectsCoreFieldMapping { type: string; null_value?: number | boolean | string; index?: boolean; + doc_values?: boolean; enabled?: boolean; fields?: { [subfield: string]: { @@ -153,6 +154,7 @@ export interface SavedObjectsCoreFieldMapping { * @public */ export interface SavedObjectsComplexFieldMapping { + doc_values?: boolean; type?: string; properties: SavedObjectsMappingProperties; } 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 376f823267eb..07675bb0a681 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 3f2c31a7c0e5..2d27ca7c8a29 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 f24195c0f295..880b71e164b5 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/server/server.api.md b/src/core/server/server.api.md index 1cabaa57e519..cb413be2c19b 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1978,6 +1978,8 @@ export interface SavedObjectsClientWrapperOptions { // @public export interface SavedObjectsComplexFieldMapping { + // (undocumented) + doc_values?: boolean; // (undocumented) properties: SavedObjectsMappingProperties; // (undocumented) @@ -1986,6 +1988,8 @@ export interface SavedObjectsComplexFieldMapping { // @public export interface SavedObjectsCoreFieldMapping { + // (undocumented) + doc_values?: boolean; // (undocumented) enabled?: boolean; // (undocumented) diff --git a/src/core/utils/deep_freeze.test.ts b/src/core/utils/deep_freeze.test.ts index 58aa9c9b8c92..48f890160d05 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/code_coverage/ingest_coverage/__tests__/either.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js index cce8fd3c2e62..3a493539f674 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js @@ -17,39 +17,39 @@ * under the License. */ -import { fromNullable, tryCatch, left, right } from '../either'; +import * as Either from '../either'; import { noop } from '../utils'; import expect from '@kbn/expect'; const pluck = (x) => (obj) => obj[x]; const expectNull = (x) => expect(x).to.equal(null); -const attempt = (obj) => fromNullable(obj).map(pluck('detail')); +const attempt = (obj) => Either.fromNullable(obj).map(pluck('detail')); describe(`either datatype functions`, () => { describe(`helpers`, () => { it(`'fromNullable' should be a fn`, () => { - expect(typeof fromNullable).to.be('function'); + expect(typeof Either.fromNullable).to.be('function'); }); - it(`'tryCatch' should be a fn`, () => { - expect(typeof tryCatch).to.be('function'); + it(`' Either.tryCatch' should be a fn`, () => { + expect(typeof Either.tryCatch).to.be('function'); }); it(`'left' should be a fn`, () => { - expect(typeof left).to.be('function'); + expect(typeof Either.left).to.be('function'); }); it(`'right' should be a fn`, () => { - expect(typeof right).to.be('function'); + expect(typeof Either.right).to.be('function'); }); }); - describe('tryCatch', () => { + describe(' Either.tryCatch', () => { let sut = undefined; it(`should return a 'Left' on error`, () => { - sut = tryCatch(() => { + sut = Either.tryCatch(() => { throw new Error('blah'); }); expect(sut.inspect()).to.be('Left(Error: blah)'); }); it(`should return a 'Right' on successful execution`, () => { - sut = tryCatch(noop); + sut = Either.tryCatch(noop); expect(sut.inspect()).to.be('Right(undefined)'); }); }); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index 2fd1d5cbe8d4..746bccc3d718 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -18,7 +18,7 @@ */ import expect from '@kbn/expect'; -import { ciRunUrl, coveredFilePath, itemizeVcs } from '../transforms'; +import { ciRunUrl, coveredFilePath, itemizeVcs, prokPrevious } from '../transforms'; describe(`Transform fn`, () => { describe(`ciRunUrl`, () => { @@ -61,6 +61,14 @@ describe(`Transform fn`, () => { }); }); }); + describe(`prokPrevious`, () => { + const comparePrefixF = () => 'https://github.com/elastic/kibana/compare'; + process.env.FETCHED_PREVIOUS = 'A'; + it(`should return a previous compare url`, () => { + const actual = prokPrevious(comparePrefixF)('B'); + expect(actual).to.be(`https://github.com/elastic/kibana/compare/A...B`); + }); + }); describe(`itemizeVcs`, () => { it(`should return a sha url`, () => { const vcsInfo = [ diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js index 2a65839f85ac..95056d9f0d8d 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js @@ -31,6 +31,7 @@ const env = { ES_HOST: 'https://super:changeme@some.fake.host:9243', NODE_ENV: 'integration_test', COVERAGE_INGESTION_KIBANA_ROOT: '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', + FETCHED_PREVIOUS: 'FAKE_PREVIOUS_SHA', }; describe('Ingesting coverage', () => { @@ -68,31 +69,64 @@ describe('Ingesting coverage', () => { expect(folderStructure.test(actualUrl)).ok(); }); }); - describe(`vcsInfo`, () => { + let stdOutWithVcsInfo = ''; describe(`without a commit msg in the vcs info file`, () => { - let vcsInfo; - const args = [ - 'scripts/ingest_coverage.js', - '--verbose', - '--vcsInfoPath', - 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO_missing_commit_msg.txt', - '--path', - ]; - beforeAll(async () => { + const args = [ + 'scripts/ingest_coverage.js', + '--verbose', + '--vcsInfoPath', + 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO_missing_commit_msg.txt', + '--path', + ]; const opts = [...args, resolved]; const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - vcsInfo = stdout; + stdOutWithVcsInfo = stdout; }); it(`should be an obj w/o a commit msg`, () => { const commitMsgRE = /"commitMsg"/; - expect(commitMsgRE.test(vcsInfo)).to.not.be.ok(); + expect(commitMsgRE.test(stdOutWithVcsInfo)).to.not.be.ok(); + }); + }); + describe(`including previous sha`, () => { + let stdOutWithPrevious = ''; + beforeAll(async () => { + const opts = [...verboseArgs, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + stdOutWithPrevious = stdout; + }); + + it(`should have a vcsCompareUrl`, () => { + const previousCompareUrlRe = /vcsCompareUrl.+\s*.*https.+compare\/FAKE_PREVIOUS_SHA\.\.\.f07b34f6206/; + expect(previousCompareUrlRe.test(stdOutWithPrevious)).to.be.ok(); + }); + }); + describe(`with a commit msg in the vcs info file`, () => { + beforeAll(async () => { + const args = [ + 'scripts/ingest_coverage.js', + '--verbose', + '--vcsInfoPath', + 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt', + '--path', + ]; + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + stdOutWithVcsInfo = stdout; + }); + + it(`should be an obj w/ a commit msg`, () => { + const commitMsgRE = /commitMsg/; + expect(commitMsgRE.test(stdOutWithVcsInfo)).to.be.ok(); }); }); }); describe(`team assignment`, () => { + let shouldNotHavePipelineOut = ''; + let shouldIndeedHavePipelineOut = ''; + const args = [ 'scripts/ingest_coverage.js', '--verbose', @@ -101,26 +135,30 @@ describe('Ingesting coverage', () => { '--path', ]; - it(`should not occur when going to the totals index`, async () => { - const teamAssignRE = /"pipeline":/; - const shouldNotHavePipelineOut = await prokJustTotalOrNot(true, args); + const teamAssignRE = /pipeline:/; + + beforeAll(async () => { + const summaryPath = 'jest-combined/coverage-summary-just-total.json'; + const resolved = resolve(MOCKS_DIR, summaryPath); + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + shouldNotHavePipelineOut = stdout; + }); + beforeAll(async () => { + const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; + const resolved = resolve(MOCKS_DIR, summaryPath); + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + shouldIndeedHavePipelineOut = stdout; + }); + + it(`should not occur when going to the totals index`, () => { const actual = teamAssignRE.test(shouldNotHavePipelineOut); expect(actual).to.not.be.ok(); }); - it(`should indeed occur when going to the coverage index`, async () => { - const shouldIndeedHavePipelineOut = await prokJustTotalOrNot(false, args); - const onlyForTestingRe = /ingest-pipe=>team_assignment/; - const actual = onlyForTestingRe.test(shouldIndeedHavePipelineOut); + it(`should indeed occur when going to the coverage index`, () => { + const actual = /ingest-pipe=>team_assignment/.test(shouldIndeedHavePipelineOut); expect(actual).to.be.ok(); }); }); }); -async function prokJustTotalOrNot(isTotal, args) { - const justTotalPath = 'jest-combined/coverage-summary-just-total.json'; - const notJustTotalPath = 'jest-combined/coverage-summary-manual-mix.json'; - - const resolved = resolve(MOCKS_DIR, isTotal ? justTotalPath : notJustTotalPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - return stdout; -} diff --git a/src/dev/code_coverage/ingest_coverage/maybe.js b/src/dev/code_coverage/ingest_coverage/maybe.js new file mode 100644 index 000000000000..89936d6fc4b0 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/maybe.js @@ -0,0 +1,84 @@ +/* + * 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. + */ + +/* eslint new-cap: 0 */ +/* eslint no-unused-vars: 0 */ + +/** + * Just monad used for valid values + */ +export function Just(x) { + return { + value: () => x, + map: (f) => Maybe.of(f(x)), + isJust: () => true, + inspect: () => `Just(${x})`, + }; +} +Just.of = function of(x) { + return Just(x); +}; +export function just(x) { + return Just.of(x); +} + +/** + * Maybe monad. + * Maybe.fromNullable` lifts an `x` into either a `Just` + * or a `Nothing` typeclass. + */ +export function Maybe(x) { + return { + chain: (f) => f(x), + map: (f) => Maybe(f(x)), + inspect: () => `Maybe(${x})`, + nothing: () => Nothing(), + isNothing: () => false, + isJust: () => false, + }; +} +Maybe.of = function of(x) { + return just(x); +}; + +export function maybe(x) { + return Maybe.of(x); +} +export function fromNullable(x) { + return x !== null && x !== undefined && x !== false && x !== 'undefined' ? just(x) : nothing(); +} + +/** + * Nothing wraps undefined or null values and prevents errors + * that otherwise occur when mapping unexpected undefined or null + * values + */ +export function Nothing() { + return { + value: () => { + throw new TypeError(`Nothing algebraic data type returns...no value :)`); + }, + map: (f) => {}, + isNothing: () => true, + inspect: () => `[Nothing]`, + }; +} +export function nothing() { + return Nothing(); +} diff --git a/src/dev/code_coverage/ingest_coverage/transforms.js b/src/dev/code_coverage/ingest_coverage/transforms.js index 4cb6c2892c4f..b8c9acd6fc49 100644 --- a/src/dev/code_coverage/ingest_coverage/transforms.js +++ b/src/dev/code_coverage/ingest_coverage/transforms.js @@ -17,10 +17,11 @@ * under the License. */ -import { left, right, fromNullable } from './either'; +import * as Either from './either'; +import { fromNullable } from './maybe'; import { always, id, noop } from './utils'; -const maybeTotal = (x) => (x === 'total' ? left(x) : right(x)); +const maybeTotal = (x) => (x === 'total' ? Either.left(x) : Either.right(x)); const trimLeftFrom = (text, x) => x.substr(x.indexOf(text)); @@ -54,13 +55,13 @@ const root = (urlBase) => (ts) => (testRunnerType) => `${urlBase}/${ts}/${testRunnerType.toLowerCase()}-combined`; const prokForTotalsIndex = (mutateTrue) => (urlRoot) => (obj) => - right(obj) + Either.right(obj) .map(mutateTrue) .map(always(`${urlRoot}/index.html`)) .fold(noop, id); const prokForCoverageIndex = (root) => (mutateFalse) => (urlRoot) => (obj) => (siteUrl) => - right(siteUrl) + Either.right(siteUrl) .map((x) => { mutateFalse(obj); return x; @@ -87,7 +88,7 @@ export const coveredFilePath = (obj) => { const withoutCoveredFilePath = always(obj); const leadingSlashRe = /^\//; - const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? right(x) : left(x)); + const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? Either.right(x) : Either.left(x)); const dropLeadingSlash = (x) => x.replace(leadingSlashRe, ''); const dropRoot = (root) => (x) => maybeDropLeadingSlash(x.replace(root, '')).fold(id, dropLeadingSlash); @@ -97,11 +98,23 @@ export const coveredFilePath = (obj) => { }; export const ciRunUrl = (obj) => - fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({ ...obj, ciRunUrl })); + Either.fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({ + ...obj, + ciRunUrl, + })); const size = 50; -const truncateMsg = (msg) => (msg.length > size ? `${msg.slice(0, 50)}...` : msg); - +const truncateMsg = (msg) => { + const res = msg.length > size ? `${msg.slice(0, 50)}...` : msg; + return res; +}; +const comparePrefix = () => 'https://github.com/elastic/kibana/compare'; +export const prokPrevious = (comparePrefixF) => (currentSha) => { + return Either.fromNullable(process.env.FETCHED_PREVIOUS).fold( + noop, + (previousSha) => `${comparePrefixF()}/${previousSha}...${currentSha}` + ); +}; export const itemizeVcs = (vcsInfo) => (obj) => { const [branch, sha, author, commitMsg] = vcsInfo; @@ -111,12 +124,23 @@ export const itemizeVcs = (vcsInfo) => (obj) => { author, vcsUrl: `https://github.com/elastic/kibana/commit/${sha}`, }; - const res = fromNullable(commitMsg).fold(always({ ...obj, vcs }), (msg) => ({ - ...obj, - vcs: { ...vcs, commitMsg: truncateMsg(msg) }, - })); - return res; + const mutateVcs = (x) => (vcs.commitMsg = truncateMsg(x)); + fromNullable(commitMsg).map(mutateVcs); + + const vcsCompareUrl = process.env.FETCHED_PREVIOUS + ? `${comparePrefix()}/${process.env.FETCHED_PREVIOUS}...${sha}` + : 'PREVIOUS SHA NOT PROVIDED'; + + // const withoutPreviousL = always({ ...obj, vcs }); + const withPreviousR = () => ({ + ...obj, + vcs: { + ...vcs, + vcsCompareUrl, + }, + }); + return withPreviousR(); }; export const testRunner = (obj) => { const { jsonSummaryPath } = obj; diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh index d3cf31fc0f42..0b67dac30747 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh @@ -14,6 +14,10 @@ CI_RUN_URL=$3 export CI_RUN_URL echo "### debug CI_RUN_URL: ${CI_RUN_URL}" +FETCHED_PREVIOUS=$4 +export FETCHED_PREVIOUS +echo "### debug FETCHED_PREVIOUS: ${FETCHED_PREVIOUS}" + ES_HOST="https://${USER_FROM_VAULT}:${PASS_FROM_VAULT}@${HOST_FROM_VAULT}" export ES_HOST diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index bc3a6265cc58..cec80dd547a5 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 7075bcf55adf..68058043477d 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 0e576a88ab36..fde1e54b0661 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 c588b941112d..82e00a99c6cf 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 fc4ff512e2bd..d76b2a2aa936 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 4f8cee2651a9..20281d8479ab 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 f075dff46679..6b7ccaed25d4 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 e9810a747c8c..7de0c8fc15f9 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 8020cef8d849..d8dbf8d6dc88 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 b5501982cec0..602b221b7d14 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 879fab206b99..ae042310fd46 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 f3fd2fde8f2c..2102b02194bc 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 577ee984e05c..3750e15c000e 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 ebf000d160b5..cf2a7859a505 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 b1999eb4b483..087e16692532 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 17da5ffca124..db1ec425f2ce 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 a852fba4a1c5..7400d84ea2ce 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 9bc1d67dd585..8a80cbef1a9c 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 019941534cad..ed2657ed3f6e 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 2c0a2d84be2c..536a6dc581db 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 3ee4d37d0b82..81d07de55faa 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 bf05f45ff856..4a34684571c3 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 1dc8a71afb19..464c03d98b83 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 a8abbba9df43..df96a58a6e99 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 79ef5e8c183d..b9a427b8da7a 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 51bde8b8605a..eb47a3e9ace7 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 93428e9f8fa4..d91834adb4a7 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 000000000000..d5a8535f3271 --- /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 74fc54b41028..522b346b6490 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 000000000000..972365e9e9d0 --- /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 000000000000..dc99f4d45082 --- /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 45e5a0704c37..b882a2bbe6e5 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 ca2e944489a7..bbca051ce31a 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 0cfcb91aa94e..b4b18e086e80 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 2e9120eb32b7..2c58d8518798 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 8b4cc0a7be1c..db6f0e5ea9ef 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/README.md b/src/plugins/charts/README.md index 319da67981aa..31727b7acb7a 100644 --- a/src/plugins/charts/README.md +++ b/src/plugins/charts/README.md @@ -18,7 +18,7 @@ Color mappings in `value`/`text` form ### `getHeatmapColors` -Funciton to retrive heatmap related colors based on `value` and `colorSchemaName` +Function to retrieve heatmap related colors based on `value` and `colorSchemaName` ### `truncatedColorSchemas` @@ -26,72 +26,4 @@ Truncated color mappings in `value`/`text` form ## Theme -the `theme` service offers utilities to interact with theme of kibana. EUI provides a light and dark theme object to work with Elastic-Charts. However, every instance of a Chart would need to pass down this the correctly EUI theme depending on Kibana's light or dark mode. There are several ways you can use the `theme` service to get the correct theme. - -> The current theme (light or dark) of Kibana is typically taken into account for the functions below. - -### `useChartsTheme` - -The simple fetching of the correct EUI theme; a **React hook**. - -```js -import { npStart } from 'ui/new_platform'; -import { Chart, Settings } from '@elastic/charts'; - -export const YourComponent = () => ( - - - -); -``` - -### `chartsTheme$` - -An **observable** of the current charts theme. Use this implementation for more flexible updates to the chart theme without full page refreshes. - -```tsx -import { npStart } from 'ui/new_platform'; -import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes'; -import { Subscription } from 'rxjs'; -import { Chart, Settings } from '@elastic/charts'; - -interface YourComponentProps {}; - -interface YourComponentState { - chartsTheme: EuiChartThemeType['theme']; -} - -export class YourComponent extends Component { - private subscription?: Subscription; - public state = { - chartsTheme: npStart.plugins.charts.theme.chartsDefaultTheme, - }; - - componentDidMount() { - this.subscription = npStart.plugins.charts.theme - .chartsTheme$ - .subscribe(chartsTheme => this.setState({ chartsTheme })); - } - - componentWillUnmount() { - if (this.subscription) { - this.subscription.unsubscribe(); - this.subscription = undefined; - } - } - - public render() { - const { chartsTheme } = this.state; - - return ( - - - - ); - } -} -``` - -### `chartsDefaultTheme` - -Returns default charts theme (i.e. light). +See Theme service [docs](public/services/theme/README.md) 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 2c9f37afc14c..e97ca8ac257b 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 fe0deac734e6..3b9e1501d638 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/charts/public/services/theme/README.md b/src/plugins/charts/public/services/theme/README.md new file mode 100644 index 000000000000..fb4f941f7934 --- /dev/null +++ b/src/plugins/charts/public/services/theme/README.md @@ -0,0 +1,92 @@ +# Theme Service + +The `theme` service offers utilities to interact with the kibana theme. EUI provides a light and dark theme object to supplement the Elastic-Charts `baseTheme`. However, every instance of a Chart would need to pass down the correct EUI theme depending on Kibana's light or dark mode. There are several ways you can use the `theme` service to get the correct shared `theme` and `baseTheme`. + +> The current theme (light or dark) of Kibana is typically taken into account for the functions below. + +## `chartsDefaultBaseTheme` + +Default `baseTheme` from `@elastic/charts` (i.e. light). + +## `chartsDefaultTheme` + +Default `theme` from `@elastic/eui` (i.e. light). + +## `useChartsTheme` and `useChartsBaseTheme` + +A **React hook** for simple fetching of the correct EUI `theme` and `baseTheme`. + +```js +import { npStart } from 'ui/new_platform'; +import { Chart, Settings } from '@elastic/charts'; + +export const YourComponent = () => ( + + + {/* ... */} + +); +``` + +## `chartsTheme$` and `chartsBaseTheme$` + +An **`Observable`** of the current charts `theme` and `baseTheme`. Use this implementation for more flexible updates to the chart theme without full page refreshes. + +```tsx +import { npStart } from 'ui/new_platform'; +import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes'; +import { Subscription, combineLatest } from 'rxjs'; +import { Chart, Settings, Theme } from '@elastic/charts'; + +interface YourComponentProps {}; + +interface YourComponentState { + chartsTheme: EuiChartThemeType['theme']; + chartsBaseTheme: Theme; +} + +export class YourComponent extends Component { + private subscriptions: Subscription[] = []; + + public state = { + chartsTheme: npStart.plugins.charts.theme.chartsDefaultTheme, + chartsBaseTheme: npStart.plugins.charts.theme.chartsDefaultBaseTheme, + }; + + componentDidMount() { + this.subscription = combineLatest( + npStart.plugins.charts.theme.chartsTheme$, + npStart.plugins.charts.theme.chartsBaseTheme$ + ).subscribe(([chartsTheme, chartsBaseTheme]) => + this.setState({ chartsTheme, chartsBaseTheme }) + ); + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public render() { + const { chartsBaseTheme, chartsTheme } = this.state; + + return ( + + + {/* ... */} + + ); + } +} +``` + +## Why have `theme` and `baseTheme`? + +The `theme` prop is a recursive partial `Theme` that overrides properties from the `baseTheme`. This allows changes to the `Theme` TS type in `@elastic/charts` without having to update the `@elastic/eui` themes for every ``. diff --git a/src/plugins/charts/public/services/theme/mock.ts b/src/plugins/charts/public/services/theme/mock.ts index 8aa1a4f2368a..7fecb862a3c6 100644 --- a/src/plugins/charts/public/services/theme/mock.ts +++ b/src/plugins/charts/public/services/theme/mock.ts @@ -21,9 +21,17 @@ import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { ThemeService } from './theme'; export const themeServiceMock: ThemeService = { + chartsDefaultTheme: EUI_CHARTS_THEME_LIGHT.theme, chartsTheme$: jest.fn(() => ({ - subsribe: jest.fn(), + subscribe: jest.fn(), })), - chartsDefaultTheme: EUI_CHARTS_THEME_LIGHT.theme, - useChartsTheme: jest.fn(), + chartsBaseTheme$: jest.fn(() => ({ + subscribe: jest.fn(), + })), + darkModeEnabled$: jest.fn(() => ({ + subscribe: jest.fn(), + })), + useDarkMode: jest.fn().mockReturnValue(false), + useChartsTheme: jest.fn().mockReturnValue({}), + useChartsBaseTheme: jest.fn().mockReturnValue({}), } as any; diff --git a/src/plugins/charts/public/services/theme/theme.test.tsx b/src/plugins/charts/public/services/theme/theme.test.tsx index fca503e387ea..52bc78dfec7d 100644 --- a/src/plugins/charts/public/services/theme/theme.test.tsx +++ b/src/plugins/charts/public/services/theme/theme.test.tsx @@ -25,15 +25,35 @@ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist import { ThemeService } from './theme'; import { coreMock } from '../../../../../core/public/mocks'; +import { LIGHT_THEME, DARK_THEME } from '@elastic/charts'; const { uiSettings: setupMockUiSettings } = coreMock.createSetup(); describe('ThemeService', () => { - describe('chartsTheme$', () => { + describe('darkModeEnabled$', () => { it('should throw error if service has not been initialized', () => { const themeService = new ThemeService(); - expect(() => themeService.chartsTheme$).toThrowError(); + expect(() => themeService.darkModeEnabled$).toThrowError(); + }); + + it('returns the false when not in dark mode', async () => { + setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false)); + const themeService = new ThemeService(); + themeService.init(setupMockUiSettings); + + expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toBe(false); + }); + + it('returns the true when in dark mode', async () => { + setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(true)); + const themeService = new ThemeService(); + themeService.init(setupMockUiSettings); + + expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toBe(true); }); + }); + + describe('chartsTheme$', () => { it('returns the light theme when not in dark mode', async () => { setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false)); const themeService = new ThemeService(); @@ -58,6 +78,28 @@ describe('ThemeService', () => { }); }); + describe('chartsBaseTheme$', () => { + it('returns the light theme when not in dark mode', async () => { + setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false)); + const themeService = new ThemeService(); + themeService.init(setupMockUiSettings); + + expect(await themeService.chartsBaseTheme$.pipe(take(1)).toPromise()).toEqual(LIGHT_THEME); + }); + + describe('in dark mode', () => { + it(`returns the dark theme`, async () => { + // Fake dark theme turned returning true + setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(true)); + const themeService = new ThemeService(); + themeService.init(setupMockUiSettings); + const result = await themeService.chartsBaseTheme$.pipe(take(1)).toPromise(); + + expect(result).toEqual(DARK_THEME); + }); + }); + }); + describe('useChartsTheme', () => { it('updates when the uiSettings change', () => { const darkMode$ = new BehaviorSubject(false); @@ -75,4 +117,22 @@ describe('ThemeService', () => { expect(result.current).toBe(EUI_CHARTS_THEME_LIGHT.theme); }); }); + + describe('useBaseChartTheme', () => { + it('updates when the uiSettings change', () => { + const darkMode$ = new BehaviorSubject(false); + setupMockUiSettings.get$.mockReturnValue(darkMode$); + const themeService = new ThemeService(); + themeService.init(setupMockUiSettings); + const { useChartsBaseTheme } = themeService; + + const { result } = renderHook(() => useChartsBaseTheme()); + expect(result.current).toBe(LIGHT_THEME); + + act(() => darkMode$.next(true)); + expect(result.current).toBe(DARK_THEME); + act(() => darkMode$.next(false)); + expect(result.current).toBe(LIGHT_THEME); + }); + }); }); diff --git a/src/plugins/charts/public/services/theme/theme.ts b/src/plugins/charts/public/services/theme/theme.ts index e1e71573caa3..2d0c4de88321 100644 --- a/src/plugins/charts/public/services/theme/theme.ts +++ b/src/plugins/charts/public/services/theme/theme.ts @@ -18,34 +18,56 @@ */ import { useEffect, useState } from 'react'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { Observable, BehaviorSubject } from 'rxjs'; import { CoreSetup } from 'kibana/public'; -import { RecursivePartial, Theme } from '@elastic/charts'; +import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; export class ThemeService { - private _chartsTheme$?: Observable>; - /** Returns default charts theme */ public readonly chartsDefaultTheme = EUI_CHARTS_THEME_LIGHT.theme; + public readonly chartsDefaultBaseTheme = LIGHT_THEME; + + private _uiSettingsDarkMode$?: Observable; + private _chartsTheme$ = new BehaviorSubject(this.chartsDefaultTheme); + private _chartsBaseTheme$ = new BehaviorSubject(this.chartsDefaultBaseTheme); /** An observable of the current charts theme */ - public get chartsTheme$(): Observable> { - if (!this._chartsTheme$) { + public chartsTheme$ = this._chartsTheme$.asObservable(); + + /** An observable of the current charts base theme */ + public chartsBaseTheme$ = this._chartsBaseTheme$.asObservable(); + + /** An observable boolean for dark mode of kibana */ + public get darkModeEnabled$(): Observable { + if (!this._uiSettingsDarkMode$) { throw new Error('ThemeService not initialized'); } - return this._chartsTheme$; + return this._uiSettingsDarkMode$; } + /** A React hook for consuming the dark mode value */ + public useDarkMode = (): boolean => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [value, update] = useState(false); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + const s = this.darkModeEnabled$.subscribe(update); + return () => s.unsubscribe(); + }, []); + + return value; + }; + /** A React hook for consuming the charts theme */ - public useChartsTheme = () => { - /* eslint-disable-next-line react-hooks/rules-of-hooks */ + public useChartsTheme = (): PartialTheme => { + // eslint-disable-next-line react-hooks/rules-of-hooks const [value, update] = useState(this.chartsDefaultTheme); - /* eslint-disable-next-line react-hooks/rules-of-hooks */ + // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { const s = this.chartsTheme$.subscribe(update); return () => s.unsubscribe(); @@ -54,12 +76,28 @@ export class ThemeService { return value; }; + /** A React hook for consuming the charts theme */ + public useChartsBaseTheme = (): Theme => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [value, update] = useState(this.chartsDefaultBaseTheme); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + const s = this.chartsBaseTheme$.subscribe(update); + return () => s.unsubscribe(); + }, []); + + return value; + }; + /** initialize service with uiSettings */ public init(uiSettings: CoreSetup['uiSettings']) { - this._chartsTheme$ = uiSettings - .get$('theme:darkMode') - .pipe( - map((darkMode) => (darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme)) + this._uiSettingsDarkMode$ = uiSettings.get$('theme:darkMode'); + this._uiSettingsDarkMode$.subscribe((darkMode) => { + this._chartsTheme$.next( + darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme ); + this._chartsBaseTheme$.next(darkMode ? DARK_THEME : LIGHT_THEME); + }); } } diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index 377e739a0c59..ebcc2a35b611 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 b7cc8f2f4b72..06823a981af4 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 f37b3ac0cca9..d31507626146 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 b770638a61ff..b26a22343333 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 79a332624e5e..412fda16d45b 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 38be0d8a7e4c..b893218f4967 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 a237fe5dd59d..037f4b1b27c5 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 aafb234b0f44..0e3b6a345836 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 22aae8da030d..88fe195bcbf2 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 28a971794d40..38592e66bd8b 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 272f63322ffa..a16fb1dadfbc 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 ccd3b6b1c0a8..ce990e62a228 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 96210358c05e..26af13b4410f 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 57fe4acf0814..e4a98ffac7a5 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 58477d28f908..a321bc7959c5 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 79116a57869d..a6928c0608bd 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 1b060c186db9..5ecd57d670ae 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 e3b6725ce744..72d3ffe6b232 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 4f7945d6dd60..1e8356a1ef10 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 75e169b79f32..452d68aa9239 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 ae9d1c792195..261977b85965 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 990d58835944..4441155ad921 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 c318a0f0c2c3..c355a7397797 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 89aec6e55e81..404f27b38992 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 a6c4a1133a2e..c9ab9df920e1 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 222960199449..2b8f9ad48a34 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 26f07a12067c..9e4308d6fd55 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 9325485bce75..74a942b51583 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 99b49b401a8b..0bad47d9889f 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 26f1a185ada3..1702441aa4ca 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 c194687b7c3b..c1aa2efe4699 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 bde550c660a3..dab11ad0ce29 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 65df6e26a25b..be8e9b13e7cf 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 8f1d02c5ffd5..345dd3b32691 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 f19611bc1d52..670b40e7d947 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 60a49a4bd50f..eaf6ddc9afc3 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 432a763bfd48..723001297e8f 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 d2d5a4b06921..41457a01e0c9 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 8650f5920e52..de49e9ab6f66 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 121bb29f6f8e..f3efeb028665 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 4052c0b39015..cb17ef07a930 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 018fcb365b58..ed9bc5e0462f 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 12197c85f4a9..017f646258c0 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 47da7e59af5e..8dc8b786fcfc 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 00d866e6f2b3..25d3a3ea90a4 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/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 43dba150bf8d..fdd952e2207d 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -109,6 +109,7 @@ function FilterBarUI(props: Props) { panelPaddingSize="none" ownFocus={true} initialFocus=".filterEditor__hiddenItem" + repositionOnScroll >
diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx index 3fb7f198d546..b97e0e33f240 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx @@ -167,6 +167,7 @@ class FilterOptionsUI extends Component { anchorPosition="rightUp" panelPaddingSize="none" withTitle + repositionOnScroll > setIsPopoverOpen(false)} withTitle + repositionOnScroll > {i18n.translate('data.noDataPopover.content', { defaultMessage: - "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts", + "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts.", })}

@@ -66,11 +66,13 @@ export function NoDataPopover({ step={1} stepsTotal={1} isStepOpen={noDataPopoverVisible} - subtitle={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Tip' })} - title="" + subtitle={i18n.translate('data.noDataPopover.subtitle', { defaultMessage: 'Tip' })} + title={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Empty dataset' })} footerAction={ { storage.set(NO_DATA_POPOVER_STORAGE_KEY, true); 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 6108de028018..8582f4a12fa3 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,13 +202,12 @@ export function SavedQueryManagementComponent({ id="savedQueryPopover" button={savedQueryPopoverButton} isOpen={isOpen} - closePopover={() => { - setIsOpen(false); - }} + closePopover={handleClosePopover} anchorPosition="downLeft" panelPaddingSize="none" buffer={-8} ownFocus + repositionOnScroll >
onSave()} + onClick={handleSave} aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel', { @@ -255,7 +281,7 @@ export function SavedQueryManagementComponent({ onSaveAsNew()} + onClick={handleSaveAsNew} aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel', { @@ -279,7 +305,7 @@ export function SavedQueryManagementComponent({ onSave()} + onClick={handleSave} aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel', { defaultMessage: 'Save a new saved query' } @@ -298,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 a0df7604f23a..f8b7e4f48091 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 502364cdcba3..b4b86b73a5f4 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 a01d34dbe9df..2e408d7569be 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.ts b/src/plugins/data/server/saved_objects/index.ts index 432620014117..102183fc1c5e 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -export { searchSavedObjectType } from './search'; export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; export { kqlTelemetry } from './kql_telementry'; 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 37819a13b651..768041a376ad 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 902cf2988f42..44d2813f6e3e 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/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index df809b425eb9..34ed8c6c6f40 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -27,7 +27,6 @@ import { } from './types'; import { registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; -import { searchSavedObjectType } from '../saved_objects'; import { DataPluginStart } from '../plugin'; export class SearchService implements Plugin { @@ -36,8 +35,6 @@ export class SearchService implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup): ISearchSetup { - core.savedObjects.registerType(searchSavedObjectType); - this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$) 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 0e057e0a715c..32fc2873d7f2 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/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index 9afe5e48bc5b..4c39c8bb2554 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -40,12 +40,13 @@ import { ElementClickListener, XYChartElementEvent, BrushEndListener, + Theme, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/public'; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; -import { Subscription } from 'rxjs'; +import { Subscription, combineLatest } from 'rxjs'; import { getServices } from '../../../kibana_services'; import { Chart as IChart } from '../helpers/point_series'; @@ -56,6 +57,7 @@ export interface DiscoverHistogramProps { interface DiscoverHistogramState { chartsTheme: EuiChartThemeType['theme']; + chartsBaseTheme: Theme; } function findIntervalFromDuration( @@ -126,18 +128,21 @@ export class DiscoverHistogram extends Component this.setState({ chartsTheme }) + this.subscription = combineLatest( + getServices().theme.chartsTheme$, + getServices().theme.chartsBaseTheme$ + ).subscribe(([chartsTheme, chartsBaseTheme]) => + this.setState({ chartsTheme, chartsBaseTheme }) ); } componentWillUnmount() { if (this.subscription) { this.subscription.unsubscribe(); - this.subscription = undefined; } } @@ -204,7 +209,7 @@ export class DiscoverHistogram extends Component { public setup(core: CoreSetup) { core.capabilities.registerProvider(capabilitiesProvider); core.uiSettings.register(uiSettings); + core.savedObjects.registerType(searchSavedObjectType); return {}; } diff --git a/typings/lodash.topath/index.d.ts b/src/plugins/discover/server/saved_objects/index.ts similarity index 87% rename from typings/lodash.topath/index.d.ts rename to src/plugins/discover/server/saved_objects/index.ts index 3630a13f72c2..efe785364ccb 100644 --- a/typings/lodash.topath/index.d.ts +++ b/src/plugins/discover/server/saved_objects/index.ts @@ -17,7 +17,4 @@ * under the License. */ -declare module 'lodash/internal/toPath' { - function toPath(value: string | string[]): string[]; - export = toPath; -} +export { searchSavedObjectType } from './search'; diff --git a/src/plugins/data/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts similarity index 84% rename from src/plugins/data/server/saved_objects/search.ts rename to src/plugins/discover/server/saved_objects/search.ts index 437c83f67bf5..2348d89c4f4d 100644 --- a/src/plugins/data/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -18,7 +18,7 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { searchSavedObjectTypeMigrations } from './search_migrations'; +import { searchMigrations } from './search_migrations'; export const searchSavedObjectType: SavedObjectsType = { name: 'search', @@ -43,18 +43,18 @@ export const searchSavedObjectType: SavedObjectsType = { }, mappings: { properties: { - columns: { type: 'keyword' }, + columns: { type: 'keyword', index: false }, description: { type: 'text' }, - hits: { type: 'integer' }, + hits: { type: 'integer', index: false }, kibanaSavedObjectMeta: { properties: { - searchSourceJSON: { type: 'text' }, + searchSourceJSON: { type: 'text', index: false }, }, }, - sort: { type: 'keyword' }, + sort: { type: 'keyword', index: false }, title: { type: 'text' }, version: { type: 'integer' }, }, }, - migrations: searchSavedObjectTypeMigrations, + migrations: searchMigrations as any, }; diff --git a/src/plugins/data/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts similarity index 96% rename from src/plugins/data/server/saved_objects/search_migrations.test.ts rename to src/plugins/discover/server/saved_objects/search_migrations.test.ts index 69db08a68925..babd25c03dbb 100644 --- a/src/plugins/data/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -18,13 +18,13 @@ */ import { SavedObjectMigrationContext } from 'kibana/server'; -import { searchSavedObjectTypeMigrations } from './search_migrations'; +import { searchMigrations } from './search_migrations'; const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; describe('migration search', () => { describe('6.7.2', () => { - const migrationFn = searchSavedObjectTypeMigrations['6.7.2']; + const migrationFn = searchMigrations['6.7.2']; it('should migrate obsolete match_all query', () => { const migratedDoc = migrationFn( @@ -56,7 +56,7 @@ describe('migration search', () => { }); describe('7.0.0', () => { - const migrationFn = searchSavedObjectTypeMigrations['7.0.0']; + const migrationFn = searchMigrations['7.0.0']; test('skips errors when searchSourceJSON is null', () => { const doc = { @@ -278,7 +278,7 @@ Object { }); describe('7.4.0', function () { - const migrationFn = searchSavedObjectTypeMigrations['7.4.0']; + const migrationFn = searchMigrations['7.4.0']; test('transforms one dimensional sort arrays into two dimensional arrays', () => { const doc = { diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts similarity index 89% rename from src/plugins/data/server/saved_objects/search_migrations.ts rename to src/plugins/discover/server/saved_objects/search_migrations.ts index 2e37cd1255ce..0302159c43c5 100644 --- a/src/plugins/data/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -19,10 +19,10 @@ import { flow, get } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; -import { DEFAULT_QUERY_LANGUAGE } from '../../common'; +import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; const migrateMatchAllQuery: SavedObjectMigrationFn = (doc) => { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { let searchSource: any; @@ -121,8 +121,8 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; }; -export const searchSavedObjectTypeMigrations = { - '6.7.2': flow>(migrateMatchAllQuery), - '7.0.0': flow>(setNewReferences), - '7.4.0': flow>(migrateSearchSortToNestedArray), +export const searchMigrations = { + '6.7.2': flow(migrateMatchAllQuery), + '7.0.0': flow(setNewReferences), + '7.4.0': flow(migrateSearchSortToNestedArray), }; 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 18e9ffcb27c5..cde2a253d763 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 5667220881df..39b91a2e20b5 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 5fbe3d2bbbdd..210b0cedccd0 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 0a2c7bb65195..adc68a39a4a5 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 28baa3d8372f..67c1ee3c7d67 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 dc8321aa0700..019a0e8053d0 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/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 2e83d16dd778..4e73d27c1c4a 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -475,17 +475,6 @@ describe('Execution', () => { } }); - test('sets duration to 10 milliseconds when function executes 10 milliseconds', async () => { - const execution = createExecution('sleep 10', {}, true); - execution.start(-1); - await execution.result; - - const node = execution.state.get().ast.chain[0]; - expect(typeof node.debug?.duration).toBe('number'); - expect(node.debug?.duration).toBeLessThan(50); - expect(node.debug?.duration).toBeGreaterThanOrEqual(5); - }); - test('adds .debug field in expression AST on each executed function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); 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 b8be273d7bbd..2b7d1b8ed9d7 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 c113765f8e7e..5cd53df663e1 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 7f2f3c37c587..e226f3b124ee 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 9428d7db1d9d..f957f10a9aeb 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/home/public/assets/googlecloud_metrics/screenshot.png b/src/plugins/home/public/assets/googlecloud_metrics/screenshot.png new file mode 100644 index 000000000000..d4d90d27ad30 Binary files /dev/null and b/src/plugins/home/public/assets/googlecloud_metrics/screenshot.png differ diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts index 332524014764..210d56369666 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts @@ -82,7 +82,7 @@ export interface TutorialSchema { name: string; isBeta?: boolean; shortDescription: string; - euiIconType?: IconType; // EUI icon type string, one of https://elastic.github.io/eui/#/icon; + euiIconType?: IconType; // EUI icon type string, one of https://elastic.github.io/eui/#/display/icons; longDescription: string; completionTimeMinutes?: number; previewImagePath?: string; diff --git a/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts b/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts new file mode 100644 index 000000000000..504ede04c12d --- /dev/null +++ b/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/metricbeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function googlecloudMetricsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'googlecloud'; + return { + id: 'googlecloudMetrics', + name: i18n.translate('home.tutorials.googlecloudMetrics.nameTitle', { + defaultMessage: 'Google Cloud metrics', + }), + category: TutorialsCategory.METRICS, + shortDescription: i18n.translate('home.tutorials.googlecloudMetrics.shortDescription', { + defaultMessage: + 'Fetch monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API.', + }), + longDescription: i18n.translate('home.tutorials.googlecloudMetrics.longDescription', { + defaultMessage: + 'The `googlecloud` Metricbeat module fetches monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-googlecloud.html', + }, + }), + euiIconType: 'logoGCP', + isBeta: false, + artifacts: { + dashboards: [ + { + id: 'f40ee870-5e4a-11ea-a4f6-717338406083', + linkLabel: i18n.translate( + 'home.tutorials.googlecloudMetrics.artifacts.dashboards.linkLabel', + { + defaultMessage: 'Google Cloud metrics dashboard', + } + ), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-googlecloud.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/googlecloud_metrics/screenshot.png', + onPrem: onPremInstructions(moduleName, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName), + }; +} diff --git a/src/plugins/home/server/tutorials/register.ts b/src/plugins/home/server/tutorials/register.ts index d13cce1c2278..c48423edb2a0 100644 --- a/src/plugins/home/server/tutorials/register.ts +++ b/src/plugins/home/server/tutorials/register.ts @@ -91,6 +91,7 @@ import { openmetricsMetricsSpecProvider } from './openmetrics_metrics'; import { oracleMetricsSpecProvider } from './oracle_metrics'; import { iisMetricsSpecProvider } from './iis_metrics'; import { azureLogsSpecProvider } from './azure_logs'; +import { googlecloudMetricsSpecProvider } from './googlecloud_metrics'; export const builtInTutorials = [ systemLogsSpecProvider, @@ -168,4 +169,5 @@ export const builtInTutorials = [ oracleMetricsSpecProvider, iisMetricsSpecProvider, azureLogsSpecProvider, + googlecloudMetricsSpecProvider, ]; 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 52cd5b0c3f5b..5ab9c695caaa 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 4eff5112c0c0..03ed6c5520de 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 70af6b5b51d1..af10d1b77b16 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 c0e0153c6053..5a970cc6cff3 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 87fdf0730c88..2fa1debf51b5 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_usage_collection/README.md b/src/plugins/kibana_usage_collection/README.md index 1aade472c232..6ef4f19c1570 100644 --- a/src/plugins/kibana_usage_collection/README.md +++ b/src/plugins/kibana_usage_collection/README.md @@ -2,7 +2,7 @@ This plugin registers the basic usage collectors from Kibana: -- Application Usage +- [Application Usage](./server/collectors/application_usage/README.md) - UI Metrics - Ops stats - Number of Saved Objects per type diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md new file mode 100644 index 000000000000..1ffd01fc6fde --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md @@ -0,0 +1,37 @@ +# Application Usage + +This collector reports the number of general clicks and minutes on screen for each registered application in Kibana. + +The final payload matches the following contract: + +```JSON +{ + "application_usage": { + "application_ID": { + "clicks_7_days": 10, + "clicks_30_days": 100, + "clicks_90_days": 300, + "clicks_total": 600, + "minutes_on_screen_7_days": 10.40, + "minutes_on_screen_30_days": 20.0, + "minutes_on_screen_90_days": 110.1, + "minutes_on_screen_total": 112.5 + } + } +} +``` + +Where `application_ID` matches the `id` registered when calling the method `core.application.register`. +This collection occurs by default for every application registered via the mentioned method and there is no need to do anything else to enable it or _opt-in_ for your plugin. + +**Note to maintainers in the Kibana repo:** At the moment of writing, the `usageCollector.schema` is not updated automatically ([#70622](https://github.com/elastic/kibana/issues/70622)) so, if you are adding a new app to Kibana, you'll need to give the Kibana Telemetry team a heads up to update the mappings in the Telemetry Cluster accordingly. + +## Developer notes + +In order to keep the count of the events, this collector uses 2 Saved Objects: + +1. `application_usage_transactional`: It stores each individually reported event (up to 90 days old). Grouped by `timestamp` and `appId`. +2. `application_usage_totals`: It stores the sum of all the events older than 90 days old per `appId`. + +Both of them use the shared fields `appId: 'keyword'`, `numberOfClicks: 'long'` and `minutesOnScreen: 'float'`. `application_usage_transactional` also stores `timestamp: { type: 'date' }`. +but they are currently not added in the mappings because we don't use them for search purposes, and we need to be thoughtful with the number of mapped fields in the SavedObjects index ([#43673](https://github.com/elastic/kibana/issues/43673)). diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts index 8d6a2d110efe..551c6e230972 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts @@ -35,13 +35,10 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe hidden: false, namespaceType: 'agnostic', mappings: { + // Not indexing any of its contents because we use them "as-is" and don't search by these fields + // for more info, see the README.md for application_usage dynamic: false, - properties: { - // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673) - // appId: { type: 'keyword' }, - // numberOfClicks: { type: 'long' }, - // minutesOnScreen: { type: 'float' }, - }, + properties: {}, }, }); @@ -53,10 +50,6 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe dynamic: false, properties: { timestamp: { type: 'date' }, - // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673) - // appId: { type: 'keyword' }, - // numberOfClicks: { type: 'long' }, - // minutesOnScreen: { type: 'float' }, }, }, }); 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 fb60f0ceff10..fe8cf12d0d6f 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 3a05ce59f5d1..1c5642f9b75b 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 5f424751dd58..dcfb44d8a522 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 75692777f08b..dbbea4012aba 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 4ce500b2da4d..f4b86b0c3119 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 39abddb3de85..ef2f937c8547 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 a0bc0d78a288..37e95f2419b4 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 6eaef3050029..a3998cbd5954 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 9a9933b5e1e8..04d0df27927f 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 0d21eb04c12b..f6354027ab01 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 4af41f67bc52..dd9e432fa512 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 26567d05e042..b2c7bcafa15a 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 54520b85cb5e..d95a6252331b 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 5e8a46374818..438582676261 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 db29d9112be8..860b4e9f2dbd 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 c02f43818af9..7be18a4774d9 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 06db7bd0e932..09518a328648 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 59adea30730c..2ee8deb4dd04 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 9b4fdddc2186..11004d2784d3 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 536145a6b8dc..6a2b7c0f5a9f 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 83466e263cf2..3d53fc8d5bd0 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 d6cb63b7c427..699342cff6a7 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 cc13b75fde12..1a5d78676fc7 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 7a34b5ec98ff..412049c89ef2 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 409372da2472..fbae9c5afffe 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 bc0e368fbdab..e407636c4156 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 108eb0c72f19..fdaa4dcd8c09 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 a7ecb4d5b473..2b9ab08f02ed 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 bf797bb5aa34..74d616cffd52 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 88ec9fecd904..36c5dc708f86 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 0363ba486a77..fcb22a9e7970 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 beb691f4b711..0638c6e67f5e 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/public/application/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss index 3db09bace079..c445d456a170 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss @@ -2,7 +2,11 @@ display: flex; flex-direction: column; flex: 1 1 100%; - padding: $euiSizeS; + + // border used in lieu of padding to prevent overlapping background-color + border-width: $euiSizeS; + border-style: solid; + border-color: transparent; .tvbVisTimeSeries { overflow: hidden; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index ddfaf3c1428d..612a7a48bade 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -34,7 +34,7 @@ import { getInterval } from '../../lib/get_interval'; import { areFieldsDifferent } from '../../lib/charts'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; -import { getCoreStart, getUISettings } from '../../../../services'; +import { getCoreStart } from '../../../../services'; export class TimeseriesVisualization extends Component { static propTypes = { @@ -154,7 +154,7 @@ export class TimeseriesVisualization extends Component { const styles = reactCSS({ default: { tvbVis: { - backgroundColor: get(model, 'background_color'), + borderColor: get(model, 'background_color'), }, }, }); @@ -237,7 +237,6 @@ export class TimeseriesVisualization extends Component { } }); - const darkMode = getUISettings().get('theme:darkMode'); return (
values.map(({ key, docs }) => ({ @@ -56,7 +56,6 @@ const handleCursorUpdate = (cursor) => { }; export const TimeSeries = ({ - darkMode, backgroundColor, showGrid, legend, @@ -90,15 +89,15 @@ export const TimeSeries = ({ const timeZone = getTimezone(uiSettings); const hasBarChart = series.some(({ bars }) => bars?.show); - // compute the theme based on the bg color - const theme = getTheme(darkMode, backgroundColor); // apply legend style change if bgColor is configured const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); // If the color isn't configured by the user, use the color mapping service // to assign a color from the Kibana palette. Colors will be shared across the // session, including dashboards. - const { colors } = getChartsSetup(); + const { colors, theme: themeService } = getChartsSetup(); + const baseTheme = getBaseTheme(themeService.useChartsBaseTheme(), backgroundColor); + colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label)); const onBrushEndListener = ({ x }) => { @@ -118,7 +117,7 @@ export const TimeSeries = ({ onBrushEnd={onBrushEndListener} animateData={false} onPointerUpdate={handleCursorUpdate} - theme={ + theme={[ hasBarChart ? {} : { @@ -127,9 +126,14 @@ export const TimeSeries = ({ fill: '#F00', }, }, - } - } - baseTheme={theme} + }, + { + background: { + color: backgroundColor, + }, + }, + ]} + baseTheme={baseTheme} tooltip={{ snap: true, type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor, @@ -269,7 +273,6 @@ TimeSeries.defaultProps = { }; TimeSeries.propTypes = { - darkMode: PropTypes.bool, backgroundColor: PropTypes.string, showGrid: PropTypes.bool, legend: PropTypes.bool, diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts index 57ca38168ac2..d7e6560a8dc9 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts @@ -17,28 +17,30 @@ * under the License. */ -import { getTheme } from './theme'; +import { getBaseTheme } from './theme'; import { LIGHT_THEME, DARK_THEME } from '@elastic/charts'; describe('TSVB theme', () => { it('should return the basic themes if no bg color is specified', () => { // use original dark/light theme - expect(getTheme(false)).toEqual(LIGHT_THEME); - expect(getTheme(true)).toEqual(DARK_THEME); + expect(getBaseTheme(LIGHT_THEME)).toEqual(LIGHT_THEME); + expect(getBaseTheme(DARK_THEME)).toEqual(DARK_THEME); // discard any wrong/missing bg color - expect(getTheme(true, null)).toEqual(DARK_THEME); - expect(getTheme(true, '')).toEqual(DARK_THEME); - expect(getTheme(true, undefined)).toEqual(DARK_THEME); + expect(getBaseTheme(DARK_THEME, null)).toEqual(DARK_THEME); + expect(getBaseTheme(DARK_THEME, '')).toEqual(DARK_THEME); + expect(getBaseTheme(DARK_THEME, undefined)).toEqual(DARK_THEME); }); it('should return a highcontrast color theme for a different background', () => { // red use a near full-black color - expect(getTheme(false, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)'); + expect(getBaseTheme(LIGHT_THEME, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)'); // violet increased the text color to full white for higer contrast - expect(getTheme(false, '#ba26ff').axes.axisTitleStyle.fill).toEqual('rgb(255,255,255)'); + expect(getBaseTheme(LIGHT_THEME, '#ba26ff').axes.axisTitleStyle.fill).toEqual( + 'rgb(255,255,255)' + ); // light yellow, prefer the LIGHT_THEME fill color because already with a good contrast - expect(getTheme(false, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333'); + expect(getBaseTheme(LIGHT_THEME, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333'); }); }); diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts index 2694732aa381..0e13fd7ef68f 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts @@ -94,9 +94,15 @@ function isValidColor(color: string | null | undefined): color is string { } } -export function getTheme(darkMode: boolean, bgColor?: string | null): Theme { +/** + * compute base chart theme based on the background color + * + * @param baseTheme + * @param bgColor + */ +export function getBaseTheme(baseTheme: Theme, bgColor?: string | null): Theme { if (!isValidColor(bgColor)) { - return darkMode ? DARK_THEME : LIGHT_THEME; + return baseTheme; } const bgLuminosity = computeRelativeLuminosity(bgColor); 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 fd20ff8b024b..0f0d99bff6f1 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 f6754e5fd9ca..a9b542af68c9 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 a9c915fcfb63..6b1af6044a2c 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 7c4f3b3ec884..708e8cf15f02 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 87477332f76e..4d4660371eaa 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 5e78637ef0c0..f04d9d17eecc 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 489cb81306b3..cf98425c04ce 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 a2fe4d9249bd..f7e44ed27878 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 6b507862fb84..da046af83a49 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 e22105d5a086..5324dc5318be 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 0bfcedc5e605..bafc3199de89 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 3269f54a621d..8b7a44d95bb3 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 c5fb4761eb9e..8a1f80df9f4d 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 98d384f95a83..3633063966e1 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 37f395aab401..4c50472b9d11 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 7465f969f456..c926c456da21 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 0c27c3a2c778..60945b912e1b 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 47757593958d..dc6ac4919a4c 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 27fe722019a2..74881b9d99ae 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 1e05c48ba7da..52b7e3ede298 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 f5f1a1785bbd..94eba5a6d7ce 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 fb857b2092ee..6b1ba779605c 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/src/setup_node_env/harden.js b/src/setup_node_env/harden/index.js similarity index 80% rename from src/setup_node_env/harden.js rename to src/setup_node_env/harden/index.js index dead3db1d60b..25cb3bcd78ff 100644 --- a/src/setup_node_env/harden.js +++ b/src/setup_node_env/harden/index.js @@ -17,8 +17,5 @@ * under the License. */ -var hook = require('require-in-the-middle'); - -hook(['child_process'], function (exports, name) { - return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require -}); +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 000000000000..2add624f9f32 --- /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 fbe16e798fff..182cb8e6e118 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 12f7eb5a0a04..6a20261421b5 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 d0ff4cc06c57..9ea3cf087be9 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/dashboard/index.js b/test/functional/apps/dashboard/index.js index 1e310c1ddd26..5a30456bd59a 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -49,6 +49,7 @@ export default function ({ getService, loadTestFile }) { after(unloadCurrentData); loadTestFile(require.resolve('./empty_dashboard')); + loadTestFile(require.resolve('./url_field_formatter')); loadTestFile(require.resolve('./embeddable_rendering')); loadTestFile(require.resolve('./create_and_add_embeddables')); loadTestFile(require.resolve('./edit_embeddable_redirects')); diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/url_field_formatter.ts new file mode 100644 index 000000000000..9b05b9b777b9 --- /dev/null +++ b/test/functional/apps/dashboard/url_field_formatter.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common, dashboard, settings, timePicker, visChart } = getPageObjects([ + 'common', + 'dashboard', + 'settings', + 'timePicker', + 'visChart', + ]); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const fieldName = 'clientip'; + + const clickFieldAndCheckUrl = async (fieldLink: WebElementWrapper) => { + const fieldValue = await fieldLink.getVisibleText(); + await fieldLink.click(); + const windowHandlers = await browser.getAllWindowHandles(); + expect(windowHandlers.length).to.equal(2); + await browser.switchToWindow(windowHandlers[1]); + const currentUrl = await browser.getCurrentUrl(); + const fieldUrl = common.getHostPort() + '/app/' + fieldValue; + expect(currentUrl).to.equal(fieldUrl); + }; + + describe('Changing field formatter to Url', () => { + before(async function () { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await common.navigateToApp('settings'); + await settings.clickKibanaIndexPatterns(); + await settings.clickIndexPatternLogstash(); + await settings.filterField(fieldName); + await settings.openControlsByName(fieldName); + await settings.setFieldFormat('url'); + await settings.controlChangeSave(); + }); + + it('applied on dashboard', async () => { + await common.navigateToApp('dashboard'); + await dashboard.loadSavedDashboard('dashboard with everything'); + await dashboard.waitForRenderComplete(); + const fieldLink = await visChart.getFieldLinkInVisTable(`${fieldName}: Descending`, 1); + await clickFieldAndCheckUrl(fieldLink); + }); + + it('applied on discover', async () => { + await common.navigateToApp('discover'); + await timePicker.setAbsoluteRange( + 'Sep 19, 2017 @ 06:31:44.000', + 'Sep 23, 2018 @ 18:31:44.000' + ); + await testSubjects.click('docTableExpandToggleColumn'); + const fieldLink = await testSubjects.find(`tableDocViewRow-${fieldName}-value`); + await clickFieldAndCheckUrl(fieldLink); + }); + + afterEach(async function () { + const windowHandlers = await browser.getAllWindowHandles(); + if (windowHandlers.length > 1) { + await browser.closeCurrentWindow(); + await browser.switchToWindow(windowHandlers[0]); + } + }); + }); +} diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index c69111be6972..03db3a2b108f 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/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 673fba0c346b..590631ad48b0 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -302,6 +302,20 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr return element.getVisibleText(); } + public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) { + const tableVis = await testSubjects.find('tableVis'); + const $ = await tableVis.parseDomContent(); + const headers = $('span[ng-bind="::col.title"]') + .toArray() + .map((header: any) => $(header).text()); + const fieldColumnIndex = headers.indexOf(fieldName); + return await find.byCssSelector( + `[data-test-subj="paginated-table-body"] tr:nth-of-type(${rowIndex}) td:nth-of-type(${ + fieldColumnIndex + 1 + }) a` + ); + } + /** * If you are writing new tests, you should rather look into getTableVisContent method instead. * @deprecated Use getTableVisContent instead. diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index d6a4fc91481d..2d35551b0480 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 000000000000..170e3a8fba43 --- /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 a32782deec65..17345d430188 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 b67c1c9060a6..36bf3409a542 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 a270144bd49f..87e79b295315 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 3a71c3aa9d3d..e35ef41420dd 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/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index e511d7a8fc15..66ebe3478fbe 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -1,3 +1,46 @@ +def downloadPrevious(title) { + def vaultSecret = 'secret/gce/elastic-bekitzur/service-account/kibana' + + withGcpServiceAccount.fromVaultSecret(vaultSecret, 'value') { + kibanaPipeline.bash(''' + + gsutil -m cp -r gs://elastic-bekitzur-kibana-coverage-live/previous_pointer/previous.txt . || echo "### Previous Pointer NOT FOUND?" + + if [ -e ./previous.txt ]; then + mv previous.txt downloaded_previous.txt + echo "### downloaded_previous.txt" + cat downloaded_previous.txt + fi + + ''', title) + + def previous = sh(script: 'cat downloaded_previous.txt', label: '### Capture Previous Sha', returnStdout: true).trim() + + return previous + } +} + +def uploadPrevious(title) { + def vaultSecret = 'secret/gce/elastic-bekitzur/service-account/kibana' + + withGcpServiceAccount.fromVaultSecret(vaultSecret, 'value') { + kibanaPipeline.bash(''' + + collectPrevious() { + PREVIOUS=$(git log --pretty=format:%h -1) + echo "### PREVIOUS: ${PREVIOUS}" + echo $PREVIOUS > previous.txt + } + collectPrevious + + gsutil cp previous.txt gs://elastic-bekitzur-kibana-coverage-live/previous_pointer/ + + + ''', title) + + } +} + def uploadCoverageStaticSite(timestamp) { def uploadPrefix = "gs://elastic-bekitzur-kibana-coverage-live/" def uploadPrefixWithTimeStamp = "${uploadPrefix}${timestamp}/" @@ -67,6 +110,7 @@ EOF cat src/dev/code_coverage/www/index.html ''', "### Combine Index Partials") } + def collectVcsInfo(title) { kibanaPipeline.bash(''' predicate() { @@ -125,31 +169,31 @@ def uploadCombinedReports() { ) } -def ingestData(jobName, buildNum, buildUrl, title) { +def ingestData(jobName, buildNum, buildUrl, previousSha, title) { kibanaPipeline.bash(""" source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline # Using existing target/kibana-coverage folder - . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' + . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' ${previousSha} """, title) } -def ingestWithVault(jobName, buildNum, buildUrl, title) { +def ingestWithVault(jobName, buildNum, buildUrl, previousSha, title) { def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - ingestData(jobName, buildNum, buildUrl, title) + ingestData(jobName, buildNum, buildUrl, previousSha, title) } } } } -def ingest(jobName, buildNumber, buildUrl, timestamp, title) { +def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, title) { withEnv([ "TIME_STAMP=${timestamp}", ]) { - ingestWithVault(jobName, buildNumber, buildUrl, title) + ingestWithVault(jobName, buildNumber, buildUrl, previousSha, title) } } 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 0600ed8e3fbf..7c495ad605f6 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 2bc6f1875644..ec559c3ee479 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 4e032001809f..b5be3cfa99e5 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 4987e4dbd4e0..6c5125ea4e0e 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 0a8bc6f1e6f5..b721cb2fc563 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 605676cee363..494f2f38e8bf 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 dd8d971b7df4..2d81c2bf4e15 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 5dff06292220..aa546e08ea1b 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 6b091a5a4503..e8e6f82f1388 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 8d859a570ba9..e1e1568d2f13 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 3512ab16a371..3c66b57bb941 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 64f846d13c0b..fa4a0e40ddee 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 7491508ee074..64d3ad54a231 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 80a3471e9c30..809caeeaf608 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 f3ae0752b908..9dc1c815bf16 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 000000000000..23ea86006b88 --- /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 000000000000..458d21bfea58 --- /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 000000000000..b49f53640051 --- /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 f3c4e48df755..000000000000 --- 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/helpers.ts b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts index 689b88390810..5791dfe5b946 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts +++ b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts @@ -6,9 +6,6 @@ /* eslint-disable import/no-extraneous-dependencies */ -const RANGE_FROM = '2020-06-01T14:59:32.686Z'; -const RANGE_TO = '2020-06-16T16:59:36.219Z'; - const BASE_URL = Cypress.config().baseUrl; /** The default time in ms to wait for a Cypress command to complete */ @@ -16,20 +13,14 @@ export const DEFAULT_TIMEOUT = 60 * 1000; export function loginAndWaitForPage( url: string, - dateRange?: { to: string; from: string } + dateRange: { to: string; from: string } ) { const username = Cypress.env('elasticsearch_username'); const password = Cypress.env('elasticsearch_password'); cy.log(`Authenticating via ${username} / ${password}`); - let rangeFrom = RANGE_FROM; - let rangeTo = RANGE_TO; - if (dateRange) { - rangeFrom = dateRange.from; - rangeTo = dateRange.to; - } - - const fullUrl = `${BASE_URL}${url}?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`; + + const fullUrl = `${BASE_URL}${url}?rangeFrom=${dateRange.from}&rangeTo=${dateRange.to}`; cy.visit(fullUrl, { auth: { username, password } }); cy.viewport('macbook-15'); 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 bc807d596a27..c98e3f81b2bc 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 acccd86f3e4d..7fbce2583903 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,25 +1,18 @@ module.exports = { - "__version": "4.5.0", - "APM": { - "Transaction duration charts": { - "1": "55 ms", - "2": "28 ms", - "3": "0 ms" - } - }, + "__version": "4.9.0", "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 +24,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/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts index 361d055db9ac..c1402bbd035f 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts @@ -12,7 +12,10 @@ export const DEFAULT_TIMEOUT = 60 * 1000; Given(`a user browses the APM UI application`, () => { // open service overview page - loginAndWaitForPage(`/app/apm#/services`); + loginAndWaitForPage(`/app/apm#/services`, { + from: '2020-06-01T14:59:32.686Z', + to: '2020-06-16T16:59:36.219Z', + }); }); When(`the user inspects the opbeans-node service`, () => { @@ -34,9 +37,8 @@ Then(`should have correct y-axis ticks`, () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get(yAxisTick).eq(2).invoke('text').snapshot(); - - cy.get(yAxisTick).eq(1).invoke('text').snapshot(); - - cy.get(yAxisTick).eq(0).invoke('text').snapshot(); + // literal assertions because snapshot() doesn't retry + cy.get(yAxisTick).eq(2).should('have.text', '55 ms'); + cy.get(yAxisTick).eq(1).should('have.text', '28 ms'); + cy.get(yAxisTick).eq(0).should('have.text', '0 ms'); }); 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 809b22490abd..89dc3437c3e6 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 439003351aed..2600e5d07332 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 000000000000..9a3d7b52674b --- /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 483cc99df747..6bab95635f55 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/e2e/package.json b/x-pack/plugins/apm/e2e/package.json index 417dda4c5220..5101e64235c6 100644 --- a/x-pack/plugins/apm/e2e/package.json +++ b/x-pack/plugins/apm/e2e/package.json @@ -9,19 +9,19 @@ }, "dependencies": { "@cypress/snapshot": "^2.1.3", - "@cypress/webpack-preprocessor": "^5.2.0", + "@cypress/webpack-preprocessor": "^5.4.1", "@types/cypress-cucumber-preprocessor": "^1.14.1", - "@types/node": "^14.0.1", + "@types/node": "^14.0.14", "axios": "^0.19.2", - "cypress": "^4.5.0", - "cypress-cucumber-preprocessor": "^2.3.1", + "cypress": "^4.9.0", + "cypress-cucumber-preprocessor": "^2.5.2", "ora": "^4.0.4", - "p-limit": "^2.3.0", + "p-limit": "^3.0.1", "p-retry": "^4.2.0", - "ts-loader": "^7.0.4", - "typescript": "3.9.5", - "wait-on": "^5.0.0", + "ts-loader": "^7.0.5", + "typescript": "3.9.6", + "wait-on": "^5.0.1", "webpack": "^4.43.0", - "yargs": "^15.3.1" + "yargs": "^15.4.0" } } diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index 43cc74a197f4..bc64f2b009d5 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -106,10 +106,12 @@ yarn &> ${TMP_DIR}/e2e-yarn.log echo "" # newline echo "${bold}Static mock data (logs: ${E2E_DIR}${TMP_DIR}/ingest-data.log)${normal}" +STATIC_MOCK_FILENAME='2020-06-12.json' + # Download static data if not already done -if [ ! -e "${TMP_DIR}/events.json" ]; then - echo 'Downloading events.json...' - curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/2020-06-12.json --output ${TMP_DIR}/events.json +if [ ! -e "${TMP_DIR}/${STATIC_MOCK_FILENAME}" ]; then + echo "Downloading ${STATIC_MOCK_FILENAME}..." + curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/${STATIC_MOCK_FILENAME} --output ${TMP_DIR}/${STATIC_MOCK_FILENAME} fi # echo "Deleting existing indices (apm* and .apm*)" @@ -117,7 +119,7 @@ curl --silent --user admin:changeme -XDELETE "localhost:${ELASTICSEARCH_PORT}/.a curl --silent --user admin:changeme -XDELETE "localhost:${ELASTICSEARCH_PORT}/apm*" > /dev/null # Ingest data into APM Server -node ingest-data/replay.js --server-url http://localhost:$APM_SERVER_PORT --events ${TMP_DIR}/events.json 2>> ${TMP_DIR}/ingest-data.log +node ingest-data/replay.js --server-url http://localhost:$APM_SERVER_PORT --events ${TMP_DIR}/${STATIC_MOCK_FILENAME} 2>> ${TMP_DIR}/ingest-data.log # Abort if not all events were ingested correctly if [ $? -ne 0 ]; then diff --git a/x-pack/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock index 975154d71b85..936294052aa7 100644 --- a/x-pack/plugins/apm/e2e/yarn.lock +++ b/x-pack/plugins/apm/e2e/yarn.lock @@ -689,6 +689,14 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" +"@babel/runtime-corejs3@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz#f29fc1990307c4c57b10dbd6ce667b27159d9e0d" + integrity sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + "@babel/runtime@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" @@ -802,13 +810,14 @@ snap-shot-compare "2.8.3" snap-shot-store "1.2.3" -"@cypress/webpack-preprocessor@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.2.0.tgz#3a17b478f6e2d600e536e6dda9c2e349d25a297e" - integrity sha512-uvo0FfKL+rIXrBGS6qPIaJRD8euK+t6YoZvrTuLPnStprzlgeGfSCnCDUEMJZqFk9LwBd1NtOop+J7qNuv74ng== +"@cypress/webpack-preprocessor@^5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.4.1.tgz#eb58f6cd02932a95653c1a674cfd769da2409806" + integrity sha512-1E2BdVVXQ4wDQ7f3mXCvS9xmfTVwEoT3oqKhjAr1iNlTJpBq10Z0VNBZd3VZ3nmCTFwTuUvs735QGnRE1gQ1BA== dependencies: bluebird "3.7.1" debug "4.1.1" + lodash "4.17.15" "@cypress/xvfb@1.2.4": version "1.2.4" @@ -865,34 +874,6 @@ dependencies: any-observable "^0.3.0" -"@types/blob-util@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" - integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== - -"@types/bluebird@3.5.29": - version "3.5.29" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" - integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== - -"@types/chai-jquery@1.1.40": - version "1.1.40" - resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" - integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== - dependencies: - "@types/chai" "*" - "@types/jquery" "*" - -"@types/chai@*": - version "4.2.11" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" - integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== - -"@types/chai@4.2.7": - version "4.2.7" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d" - integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g== - "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -903,71 +884,22 @@ resolved "https://registry.yarnpkg.com/@types/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.14.1.tgz#9787f4e89553ebc6359ce157a26ad51ed14aa98b" integrity sha512-CpYsiQ49UrOmadhFg0G5RkokPUmGGctD01mOWjNxFxHw5VgIRv33L2RyFHL8klaAI4HaedGN3Tcj4HTQ65hn+A== -"@types/jquery@*": - version "3.3.38" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.38.tgz#6385f1e1b30bd2bff55ae8ee75ea42a999cc3608" - integrity sha512-nkDvmx7x/6kDM5guu/YpXkGZ/Xj/IwGiLDdKM99YA5Vag7pjGyTJ8BNUh/6hxEn/sEu5DKtyRgnONJ7EmOoKrA== - dependencies: - "@types/sizzle" "*" - -"@types/jquery@3.3.31": - version "3.3.31" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" - integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== - dependencies: - "@types/sizzle" "*" - -"@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/minimatch@3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/mocha@5.2.7": - version "5.2.7" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" - integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== - -"@types/node@^14.0.1": - version "14.0.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.1.tgz#5d93e0a099cd0acd5ef3d5bde3c086e1f49ff68c" - integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA== +"@types/node@^14.0.14": + version "14.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" + integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/sinon-chai@3.2.3": - version "3.2.3" - resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" - integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== - dependencies: - "@types/chai" "*" - "@types/sinon" "*" - -"@types/sinon@*": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.0.tgz#5b70a360f55645dd64f205defd2a31b749a59799" - integrity sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA== - dependencies: - "@types/sinonjs__fake-timers" "*" - -"@types/sinon@7.5.1": - version "7.5.1" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" - integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== - -"@types/sinonjs__fake-timers@*": +"@types/sinonjs__fake-timers@6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== -"@types/sizzle@*", "@types/sizzle@2.3.2": +"@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -1262,10 +1194,10 @@ aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" - integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== +arch@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" + integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== argparse@^1.0.7: version "1.0.10" @@ -1347,7 +1279,7 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async@^3.1.0: +async@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== @@ -2046,10 +1978,10 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" - integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== +commander@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== commander@^2.19.0, commander@^2.20.0, commander@^2.9.0: version "2.20.3" @@ -2141,6 +2073,11 @@ core-js-compat@^3.1.1: browserslist "^4.8.3" semver "7.0.0" +core-js-pure@^3.0.0: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" + integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== + core-js@^2.4.0: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" @@ -2278,10 +2215,10 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress-cucumber-preprocessor@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-2.3.1.tgz#dc9dee8d59d3c787c5c70fc4271c32e95575b083" - integrity sha512-cKa7/VsOthzvdSQSdFiLwSWtBrtDE2q/qAPDL6NWOF4Tqm/AWvvOv18b9l9Z1t4SpphezR7RGnG1QIU45y9PPw== +cypress-cucumber-preprocessor@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-2.5.2.tgz#d544616ece1fb361867e904678d970fe82398b54" + integrity sha512-djQjXmRWUKlA15GxWGhkqaeu1PalWeNrRyxij74QJ2dEp/ozQg35NeVABeWQjgjY2xTE87X6k5iC4y+Sbohe3A== dependencies: "@cypress/browserify-preprocessor" "^2.1.1" chai "^4.1.2" @@ -2297,48 +2234,39 @@ cypress-cucumber-preprocessor@^2.3.1: minimist "^1.2.0" through "^2.3.8" -cypress@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.5.0.tgz#01940d085f6429cec3c87d290daa47bb976a7c7b" - integrity sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ== +cypress@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.9.0.tgz#c188a3864ddf841c0fdc81a9e4eff5cf539cd1c1" + integrity sha512-qGxT5E0j21FPryzhb0OBjCdhoR/n1jXtumpFFSBPYWsaZZhNaBvc3XlBUDEZKkkXPsqUFYiyhWdHN/zo0t5FcA== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" - "@types/blob-util" "1.3.3" - "@types/bluebird" "3.5.29" - "@types/chai" "4.2.7" - "@types/chai-jquery" "1.1.40" - "@types/jquery" "3.3.31" - "@types/lodash" "4.14.149" - "@types/minimatch" "3.0.3" - "@types/mocha" "5.2.7" - "@types/sinon" "7.5.1" - "@types/sinon-chai" "3.2.3" + "@types/sinonjs__fake-timers" "6.0.1" "@types/sizzle" "2.3.2" - arch "2.1.1" + arch "2.1.2" bluebird "3.7.2" cachedir "2.3.0" chalk "2.4.2" check-more-types "2.24.0" cli-table3 "0.5.1" - commander "4.1.0" + commander "4.1.1" common-tags "1.8.0" debug "4.1.1" - eventemitter2 "4.1.2" + eventemitter2 "6.4.2" execa "1.0.0" executable "4.1.1" extract-zip "1.7.0" fs-extra "8.1.0" - getos "3.1.4" + getos "3.2.1" is-ci "2.0.0" - is-installed-globally "0.1.0" + is-installed-globally "0.3.2" lazy-ass "1.6.0" listr "0.14.3" lodash "4.17.15" log-symbols "3.0.0" minimist "1.2.5" - moment "2.24.0" + moment "2.26.0" ospath "1.2.2" pretty-bytes "5.3.0" ramda "0.26.1" @@ -2407,6 +2335,13 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decamelize@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-3.2.0.tgz#84b8e8f4f8c579f938e35e2cc7024907e0090851" + integrity sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw== + dependencies: + xregexp "^4.2.4" + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -2698,10 +2633,10 @@ esutils@^2.0.0, esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eventemitter2@4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15" - integrity sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU= +eventemitter2@6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" + integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== events@^2.0.0: version "2.1.0" @@ -3040,12 +2975,12 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -getos@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.1.4.tgz#29cdf240ed10a70c049add7b6f8cb08c81876faf" - integrity sha512-UORPzguEB/7UG5hqiZai8f0vQ7hzynMQyJLxStoQ8dPGAcmgsfXOPA4iE/fGtweHYkK+z4zc9V0g+CIFRf5HYw== +getos@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== dependencies: - async "^3.1.0" + async "^3.2.0" getpass@^0.1.1: version "0.1.7" @@ -3079,12 +3014,12 @@ glob@^7.0.0, glob@^7.1.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= +global-dirs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" + integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== dependencies: - ini "^1.3.4" + ini "^1.3.5" globals@^11.1.0: version "11.12.0" @@ -3261,7 +3196,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.4: +ini@^1.3.4, ini@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== @@ -3424,13 +3359,13 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" -is-installed-globally@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" - integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= +is-installed-globally@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== dependencies: - global-dirs "^0.1.0" - is-path-inside "^1.0.0" + global-dirs "^2.0.1" + is-path-inside "^3.0.1" is-interactive@^1.0.0: version "1.0.0" @@ -3456,12 +3391,10 @@ is-observable@^1.1.0: dependencies: symbol-observable "^1.1.0" -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" +is-path-inside@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" @@ -4031,10 +3964,10 @@ module-deps@^6.0.0: through2 "^2.0.0" xtend "^4.0.0" -moment@2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moment@2.26.0: + version "2.26.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" + integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== move-concurrently@^1.0.1: version "1.0.1" @@ -4319,10 +4252,10 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== +p-limit@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.1.tgz#584784ac0722d1aed09f19f90ed2999af6ce2839" + integrity sha512-mw/p92EyOzl2MhauKodw54Rx5ZK4624rNfgNaBguFZkHzyUG9WsDzFF5/yQVEJinbJDdP4jEfMN+uBquiGnaLg== dependencies: p-try "^2.0.0" @@ -4436,11 +4369,6 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -4711,6 +4639,11 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -5503,10 +5436,10 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -ts-loader@^7.0.4: - version "7.0.4" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-7.0.4.tgz#5d9b95227de5afb91fdd9668f8920eb193cfe0cc" - integrity sha512-5du6OQHl+4ZjO4crEyoYUyWSrmmo7bAO+inkaILZ68mvahqrfoa4nn0DRmpQ4ruT4l+cuJCgF0xD7SBIyLeeow== +ts-loader@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-7.0.5.tgz#789338fb01cb5dc0a33c54e50558b34a73c9c4c5" + integrity sha512-zXypEIT6k3oTc+OZNx/cqElrsbBtYqDknf48OZos0NQ3RTt045fBIU8RRSu+suObBzYB355aIPGOe/3kj9h7Ig== dependencies: chalk "^2.3.0" enhanced-resolve "^4.0.0" @@ -5561,10 +5494,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.9.5: - version "3.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" - integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== +typescript@3.9.6: + version "3.9.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" + integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== umd@^3.0.0: version "3.0.3" @@ -5740,10 +5673,10 @@ vm-browserify@^1.0.0, vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -wait-on@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.0.0.tgz#72e554b338490bbc7131362755ca1af04f46d029" - integrity sha512-6v9lttmGGRT7Lr16E/0rISTBIV1DN72n9+77Bpt1iBfzmhBI+75RDlacFe0Q+JizkmwWXmgHUcFG5cgx3Bwqzw== +wait-on@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.0.1.tgz#7dadfe83c36fdf034de996a41aa094af5cf23077" + integrity sha512-TxzkYIfRWK1hLc9IlUh9bE1mrvIIM3ptPRKQ86Z8Qo0tBQLCHEvWzkRD1Ge4FWprKflHOnAtqIBH2nKmib/lrg== dependencies: "@hapi/joi" "^17.1.1" axios "^0.19.2" @@ -5858,6 +5791,13 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +xregexp@^4.2.4: + version "4.3.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" + integrity sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g== + dependencies: + "@babel/runtime-corejs3" "^7.8.3" + xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -5878,21 +5818,21 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yargs-parser@^18.1.1: - version "18.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1" - integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ== +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@^15.3.1: - version "15.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" - integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== +yargs@^15.4.0: + version "15.4.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.0.tgz#53949fb768309bac1843de9b17b80051e9805ec2" + integrity sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw== dependencies: cliui "^6.0.0" - decamelize "^1.2.0" + decamelize "^3.2.0" find-up "^4.1.0" get-caller-file "^2.0.1" require-directory "^2.1.1" @@ -5901,7 +5841,7 @@ yargs@^15.3.1: string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^18.1.1" + yargs-parser "^18.1.2" yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" 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 7ee8dfa496b5..4e1af6e0dc23 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 d71d5f2cb480..3cd04ee032e5 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 295f343b411a..1625fb4c1409 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 c9e475ef1531..3ddaa66b8de5 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 81bdbdad805d..ce60ffa4ba4e 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 8a3e2b1a02da..26cff5e71b61 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 f0bc313ab464..054476af28de 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 c1bfce4cdca4..620ae6708eda 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 abca9817bd69..729ed9b10f82 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 e12a4a4831e1..0bb62bd8efcf 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 01043f33ec7b..b37146f3b3be 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 7f99939a0a0d..d3a9ade3925a 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 7aafa9e1fdce..de60441f4faa 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 9f72ac6d5916..447e11eab5e4 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 a26653d3d529..99822c0bbc5c 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 9ce993e84848..9745c9ffdc70 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 589199221d7a..79ccf8dbd6f9 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/readme.md b/x-pack/plugins/apm/readme.md index 778b1f2ad2d9..f460ff6ff9bf 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -81,37 +81,32 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### API integration tests Our tests are separated in two suites: one suite runs with a basic license, and the other -with a trial license (the equivalent of gold+). This requires separate test servers and test runs. +with a trial license (the equivalent of gold+). This requires separate test servers and test runners. -**Start server** - -Basic: +**Basic** ``` +# Start server node scripts/functional_tests_server --config x-pack/test/apm_api_integration/basic/config.ts -``` - -Trial: -``` -node scripts/functional_tests_server --config x-pack/test/apm_api_integration/trial/config.ts +# Run tests +node scripts/functional_test_runner --config x-pack/test/apm_api_integration/basic/config.ts ``` -**Run tests** +The API tests for "basic" are located in `x-pack/test/apm_api_integration/basic/tests`. -Basic: +**Trial** ``` -node scripts/functional_test_runner --config x-pack/test/apm_api_integration/basic/config.ts -``` - -Trial: +# Start server +node scripts/functional_tests_server --config x-pack/test/apm_api_integration/trial/config.ts -``` +# Run tests node scripts/functional_test_runner --config x-pack/test/apm_api_integration/trial/config.ts ``` -APM tests are located in `x-pack/test/apm_api_integration`. +The API tests for "trial" are located in `x-pack/test/apm_api_integration/trial/tests`. + For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### Linting 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 000000000000..1f4a8a4367fa --- /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 000000000000..08c8aba5f020 --- /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 000000000000..a3a7e5c995bf --- /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 01cbc1aa9b98..f6e331a09fa6 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 000000000000..49d8efebbf43 --- /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 835c00b8df23..2e394f44b25b 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 3f8d6b22cd00..0fc1f89a3723 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 acf052affabd..14772e77fe1c 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 000000000000..c28bcad841ff --- /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 d90cd8bf1390..b2fe7efeaf95 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 81dba39e9d71..b04ff6764675 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 5af8b9f78cec..3c48c14c2a47 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 31bc0563ec13..588d5c7896db 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 b21f0ea8d32d..92f52dd1552d 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 08eba00251e2..74ab717b8de5 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 e609cd83587c..5bf0f51f4835 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 947e22ee2908..ebac34afa016 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 94e4ca46aec1..6bbf269711fb 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 8e3f58b18f39..24a7e5c3af8f 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 9e4271c68341..63a81e089534 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 45a5303d8b0d..3148a6742f76 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 0a648e861b38..5fdc4519f3bd 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 b568f1892486..c32c553fffc1 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 4839db047c87..21166454e478 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 0b4583f4581a..4ffd2ff3e0c9 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 6fbaee8736a5..e4b710240de1 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 bae80d3c3351..f79f189f363d 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 8d28287b3206..487f17fb89d1 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 a33d000a1f65..8ae61f7197ee 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 1449bddf322b..05ecf467a1d3 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 c098b7772ef1..2bc52fb0eaaf 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 134efe61c9dc..c0ed14965cbd 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 8c9f8847d8ee..062f782942a8 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 234f50507166..b9c879a27fd9 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 e417821fd4f6..c69a1fd9b813 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 28cfac11e76b..af4e3af6db69 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 065b84c490d2..701016d6bf0a 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,13 +18,13 @@ 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'; import { TagList } from '../tag_list'; import { getTagsFilter } from '../../lib/get_tags_filter'; -// @ts-ignore untyped local +// @ts-expect-error import { extractSearch } from '../../lib/extract_search'; import { ComponentStrings } from '../../../i18n'; import { CanvasTemplate } from '../../../types'; @@ -61,7 +61,7 @@ export class WorkpadTemplates extends React.PureComponent< WorkpadTemplatesState > { static propTypes = { - createFromTemplate: PropTypes.func.isRequired, + onCreateFromTemplate: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, templates: PropTypes.object, }; @@ -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 e0fe6e60c1da..f02407ba2897 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 48f4a41c7690..ecde5d2eb255 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 7ca93f440087..f713da5419b3 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 b4b2354b4cae..714a616679bc 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 17e2712c44b8..f4e715b1bbc4 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 766e27d95da9..770d4403f858 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 0f4953ff56d9..83f4984b4a30 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 78a34a58f5f7..9cd2bdabd3f4 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 3ada8e7b4efd..7b39e8b83b04 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 420b785771bf..9f71edcc05bf 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 3d394afaeba5..4b00d061c17c 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 021ac41d88d1..9dae2047c30b 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/server/templates/index.ts b/x-pack/plugins/canvas/server/templates/index.ts index bd0abed1912c..c2723fbc87e1 100644 --- a/x-pack/plugins/canvas/server/templates/index.ts +++ b/x-pack/plugins/canvas/server/templates/index.ts @@ -13,20 +13,21 @@ import { light } from './theme_light'; import { TEMPLATE_TYPE } from '../../common/lib/constants'; -export const templates = [pitch, status, summary, dark, light]; +export const templates = [status, summary, dark, light, pitch]; export async function initializeTemplates( - client: Pick + client: Pick ) { const existingTemplates = await client.find({ type: TEMPLATE_TYPE, perPage: 1 }); if (existingTemplates.total === 0) { - const templateObjects = templates.map((template) => ({ - id: template.id, - type: TEMPLATE_TYPE, - attributes: template, - })); - - client.bulkCreate(templateObjects); + // Some devs were seeing timeouts that would cause an unhandled promise rejection + // likely because the pitch template is so huge. + // So, rather than doing a bulk create of templates, we're going to fire off individual + // creates and catch and throw-away any errors that happen. + // Once packages are ready, we should probably move that pitch that is so large to a package + for (const template of templates) { + client.create(TEMPLATE_TYPE, template, { id: template.id }).catch((err) => undefined); + } } } diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 66b0a7bc558c..1a5a21985ba7 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 24273280d949..d988de5851cf 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 546dc6361826..20b3292128a2 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 6cfb16d12642..f7f915f1cf0e 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 36a6bc0a926a..0339d0883dc4 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 e41035e9365c..2570d4540b6a 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/global_search/server/mocks.ts b/x-pack/plugins/global_search/server/mocks.ts index 8a189a570170..e7c133edf95c 100644 --- a/x-pack/plugins/global_search/server/mocks.ts +++ b/x-pack/plugins/global_search/server/mocks.ts @@ -11,6 +11,7 @@ import { RouteHandlerGlobalSearchContext, } from './types'; import { searchServiceMock } from './services/search_service.mock'; +import { contextMock } from './services/context.mock'; const createSetupMock = (): jest.Mocked => { const searchMock = searchServiceMock.createSetupContract(); @@ -29,17 +30,18 @@ const createStartMock = (): jest.Mocked => { }; const createRouteHandlerContextMock = (): jest.Mocked => { - const contextMock = { + const handlerContextMock = { find: jest.fn(), }; - contextMock.find.mockReturnValue(of([])); + handlerContextMock.find.mockReturnValue(of([])); - return contextMock; + return handlerContextMock; }; export const globalSearchPluginMock = { createSetupContract: createSetupMock, createStartContract: createStartMock, createRouteHandlerContext: createRouteHandlerContextMock, + createProviderContext: contextMock.create, }; diff --git a/x-pack/plugins/global_search/server/services/context.mock.ts b/x-pack/plugins/global_search/server/services/context.mock.ts new file mode 100644 index 000000000000..50c6da109f8d --- /dev/null +++ b/x-pack/plugins/global_search/server/services/context.mock.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + savedObjectsTypeRegistryMock, + savedObjectsClientMock, + elasticsearchServiceMock, + uiSettingsServiceMock, +} from '../../../../../src/core/server/mocks'; + +const createContextMock = () => { + return { + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + typeRegistry: savedObjectsTypeRegistryMock.create(), + }, + elasticsearch: { + legacy: { + client: elasticsearchServiceMock.createScopedClusterClient(), + }, + }, + uiSettings: { + client: uiSettingsServiceMock.createClient(), + }, + }, + }; +}; + +const createFactoryMock = () => () => () => createContextMock(); + +export const contextMock = { + create: createContextMock, + createFactory: createFactoryMock, +}; diff --git a/x-pack/plugins/global_search_providers/kibana.json b/x-pack/plugins/global_search_providers/kibana.json index 025ea2bceed2..39eca87d0bf8 100644 --- a/x-pack/plugins/global_search_providers/kibana.json +++ b/x-pack/plugins/global_search_providers/kibana.json @@ -2,7 +2,7 @@ "id": "globalSearchProviders", "version": "8.0.0", "kibanaVersion": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["globalSearch"], "optionalPlugins": [], diff --git a/x-pack/plugins/global_search_providers/server/index.ts b/x-pack/plugins/global_search_providers/server/index.ts new file mode 100644 index 000000000000..26e4142d4865 --- /dev/null +++ b/x-pack/plugins/global_search_providers/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from 'src/core/server'; +import { GlobalSearchProvidersPlugin, GlobalSearchProvidersPluginSetupDeps } from './plugin'; + +export const plugin: PluginInitializer<{}, {}, GlobalSearchProvidersPluginSetupDeps, {}> = () => + new GlobalSearchProvidersPlugin(); diff --git a/x-pack/plugins/global_search_providers/server/plugin.test.ts b/x-pack/plugins/global_search_providers/server/plugin.test.ts new file mode 100644 index 000000000000..c9b51619d178 --- /dev/null +++ b/x-pack/plugins/global_search_providers/server/plugin.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from '../../../../src/core/server/mocks'; +import { globalSearchPluginMock } from '../../global_search/server/mocks'; +import { GlobalSearchProvidersPlugin } from './plugin'; + +describe('GlobalSearchProvidersPlugin', () => { + let plugin: GlobalSearchProvidersPlugin; + let globalSearchSetup: ReturnType; + + beforeEach(() => { + plugin = new GlobalSearchProvidersPlugin(); + globalSearchSetup = globalSearchPluginMock.createSetupContract(); + }); + + describe('#setup', () => { + it('registers the `savedObjects` result provider', () => { + const coreSetup = coreMock.createSetup(); + plugin.setup(coreSetup, { globalSearch: globalSearchSetup }); + + expect(globalSearchSetup.registerResultProvider).toHaveBeenCalledTimes(1); + expect(globalSearchSetup.registerResultProvider).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'savedObjects', + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/global_search_providers/server/plugin.ts b/x-pack/plugins/global_search_providers/server/plugin.ts new file mode 100644 index 000000000000..64e7802937d8 --- /dev/null +++ b/x-pack/plugins/global_search_providers/server/plugin.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 { CoreSetup, Plugin } from 'src/core/server'; +import { GlobalSearchPluginSetup } from '../../global_search/server'; +import { createSavedObjectsResultProvider } from './providers'; + +export interface GlobalSearchProvidersPluginSetupDeps { + globalSearch: GlobalSearchPluginSetup; +} + +export class GlobalSearchProvidersPlugin + implements Plugin<{}, {}, GlobalSearchProvidersPluginSetupDeps, {}> { + setup( + { getStartServices }: CoreSetup<{}, {}>, + { globalSearch }: GlobalSearchProvidersPluginSetupDeps + ) { + globalSearch.registerResultProvider(createSavedObjectsResultProvider()); + return {}; + } + + start() { + return {}; + } +} diff --git a/x-pack/plugins/apm/typings/lodash.mean.d.ts b/x-pack/plugins/global_search_providers/server/providers/index.ts similarity index 67% rename from x-pack/plugins/apm/typings/lodash.mean.d.ts rename to x-pack/plugins/global_search_providers/server/providers/index.ts index 0b9ca3f6914c..1670871f305d 100644 --- a/x-pack/plugins/apm/typings/lodash.mean.d.ts +++ b/x-pack/plugins/global_search_providers/server/providers/index.ts @@ -4,7 +4,4 @@ * 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 { createSavedObjectsResultProvider } from './saved_objects'; diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/index.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/index.ts similarity index 79% rename from x-pack/plugins/ml/public/application/explorer/select_limit/index.ts rename to x-pack/plugins/global_search_providers/server/providers/saved_objects/index.ts index 5b7040e5c360..4a67fd8b3df1 100644 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/index.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { useSwimlaneLimit, SelectLimit } from './select_limit'; +export { createSavedObjectsResultProvider } from './provider'; diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.test.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.test.ts new file mode 100644 index 000000000000..0085331c5be5 --- /dev/null +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsFindResult, SavedObjectsType, SavedObjectTypeRegistry } from 'src/core/server'; +import { mapToResult, mapToResults } from './map_object_to_result'; + +const createType = (props: Partial): SavedObjectsType => { + return { + name: 'type', + hidden: false, + namespaceType: 'single', + mappings: { properties: {} }, + ...props, + }; +}; + +const createObject = ( + props: Partial, + attributes: T +): SavedObjectsFindResult => { + return { + id: 'id', + type: 'dashboard', + references: [], + score: 100, + ...props, + attributes, + }; +}; + +describe('mapToResult', () => { + it('converts a savedObject to a result', () => { + const type = createType({ + name: 'dashboard', + management: { + defaultSearchField: 'title', + getInAppUrl: (obj) => ({ path: `/dashboard/${obj.id}`, uiCapabilitiesPath: '' }), + }, + }); + + const obj = createObject( + { + id: 'dash1', + type: 'dashboard', + score: 42, + }, + { + title: 'My dashboard', + } + ); + + expect(mapToResult(obj, type)).toEqual({ + id: 'dash1', + title: 'My dashboard', + type: 'dashboard', + url: '/dashboard/dash1', + score: 42, + }); + }); + + it('throws if the type do not have management information', () => { + const object = createObject( + { id: 'dash1', type: 'dashboard', score: 42 }, + { title: 'My dashboard' } + ); + + expect(() => { + mapToResult( + object, + createType({ + name: 'dashboard', + management: { + getInAppUrl: (obj) => ({ path: `/dashboard/${obj.id}`, uiCapabilitiesPath: '' }), + }, + }) + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Trying to map an object from a type without management metadata"` + ); + + expect(() => { + mapToResult( + object, + createType({ + name: 'dashboard', + management: { + defaultSearchField: 'title', + }, + }) + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Trying to map an object from a type without management metadata"` + ); + + expect(() => { + mapToResult( + object, + createType({ + name: 'dashboard', + management: undefined, + }) + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Trying to map an object from a type without management metadata"` + ); + }); +}); + +describe('mapToResults', () => { + let typeRegistry: SavedObjectTypeRegistry; + + beforeEach(() => { + typeRegistry = new SavedObjectTypeRegistry(); + }); + + it('converts savedObjects to results', () => { + typeRegistry.registerType( + createType({ + name: 'typeA', + management: { + defaultSearchField: 'title', + getInAppUrl: (obj) => ({ path: `/type-a/${obj.id}`, uiCapabilitiesPath: '' }), + }, + }) + ); + typeRegistry.registerType( + createType({ + name: 'typeB', + management: { + defaultSearchField: 'description', + getInAppUrl: (obj) => ({ path: `/type-b/${obj.id}`, uiCapabilitiesPath: 'foo' }), + }, + }) + ); + typeRegistry.registerType( + createType({ + name: 'typeC', + management: { + defaultSearchField: 'excerpt', + getInAppUrl: (obj) => ({ path: `/type-c/${obj.id}`, uiCapabilitiesPath: 'bar' }), + }, + }) + ); + + const results = [ + createObject( + { + id: 'resultA', + type: 'typeA', + score: 100, + }, + { + title: 'titleA', + field: 'noise', + } + ), + createObject( + { + id: 'resultC', + type: 'typeC', + score: 42, + }, + { + excerpt: 'titleC', + title: 'foo', + } + ), + createObject( + { + id: 'resultB', + type: 'typeB', + score: 69, + }, + { + description: 'titleB', + bar: 'baz', + } + ), + ]; + + expect(mapToResults(results, typeRegistry)).toEqual([ + { + id: 'resultA', + title: 'titleA', + type: 'typeA', + url: '/type-a/resultA', + score: 100, + }, + { + id: 'resultC', + title: 'titleC', + type: 'typeC', + url: '/type-c/resultC', + score: 42, + }, + { + id: 'resultB', + title: 'titleB', + type: 'typeB', + url: '/type-b/resultB', + score: 69, + }, + ]); + }); +}); diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts new file mode 100644 index 000000000000..c93558b1a3cf --- /dev/null +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SavedObjectsType, + ISavedObjectTypeRegistry, + SavedObjectsFindResult, +} from 'src/core/server'; +import { GlobalSearchProviderResult } from '../../../../global_search/server'; + +export const mapToResults = ( + objects: Array>, + registry: ISavedObjectTypeRegistry +): GlobalSearchProviderResult[] => { + return objects.map((obj) => mapToResult(obj, registry.getType(obj.type)!)); +}; + +export const mapToResult = ( + object: SavedObjectsFindResult, + type: SavedObjectsType +): GlobalSearchProviderResult => { + const { defaultSearchField, getInAppUrl } = type.management ?? {}; + if (defaultSearchField === undefined || getInAppUrl === undefined) { + throw new Error('Trying to map an object from a type without management metadata'); + } + return { + id: object.id, + // defaultSearchField is dynamic and not 'directly' bound to the generic type of the SavedObject + // so we are forced to cast the attributes to any to access the properties associated with it. + title: (object.attributes as any)[defaultSearchField], + type: object.type, + url: getInAppUrl(object).path, + score: object.score, + }; +}; diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts new file mode 100644 index 000000000000..84e05c67c5f6 --- /dev/null +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EMPTY } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { + SavedObjectsFindResponse, + SavedObjectsFindResult, + SavedObjectsType, + SavedObjectTypeRegistry, +} from 'src/core/server'; +import { globalSearchPluginMock } from '../../../../global_search/server/mocks'; +import { + GlobalSearchResultProvider, + GlobalSearchProviderFindOptions, +} from '../../../../global_search/server'; +import { createSavedObjectsResultProvider } from './provider'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + +const createFindResponse = ( + results: SavedObjectsFindResult[] +): SavedObjectsFindResponse => ({ + saved_objects: results, + page: 1, + per_page: 20, + total: results.length, +}); + +const createType = (props: Partial): SavedObjectsType => { + return { + name: 'type', + hidden: false, + namespaceType: 'single', + mappings: { properties: {} }, + ...props, + management: { + defaultSearchField: 'field', + getInAppUrl: (obj) => ({ path: `/object/${obj.id}`, uiCapabilitiesPath: '' }), + ...props.management, + }, + }; +}; + +const createObject = ( + props: Partial, + attributes: T +): SavedObjectsFindResult => { + return { + id: 'id', + type: 'dashboard', + score: 100, + references: [], + ...props, + attributes, + }; +}; + +const defaultOption: GlobalSearchProviderFindOptions = { + preference: 'pref', + maxResults: 20, + aborted$: EMPTY, +}; + +describe('savedObjectsResultProvider', () => { + let provider: GlobalSearchResultProvider; + let registry: SavedObjectTypeRegistry; + let context: ReturnType; + + beforeEach(() => { + provider = createSavedObjectsResultProvider(); + registry = new SavedObjectTypeRegistry(); + + registry.registerType( + createType({ + name: 'typeA', + management: { + defaultSearchField: 'title', + getInAppUrl: (obj) => ({ path: `/type-a/${obj.id}`, uiCapabilitiesPath: '' }), + }, + }) + ); + registry.registerType( + createType({ + name: 'typeB', + management: { + defaultSearchField: 'description', + getInAppUrl: (obj) => ({ path: `/type-b/${obj.id}`, uiCapabilitiesPath: 'foo' }), + }, + }) + ); + + context = globalSearchPluginMock.createProviderContext(); + context.core.savedObjects.client.find.mockResolvedValue(createFindResponse([])); + context.core.savedObjects.typeRegistry = registry as any; + }); + + it('has the correct id', () => { + expect(provider.id).toBe('savedObjects'); + }); + + it('calls `savedObjectClient.find` with the correct parameters', () => { + provider.find('term', defaultOption, context); + + expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); + expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ + page: 1, + perPage: defaultOption.maxResults, + search: 'term', + preference: 'pref', + searchFields: ['title', 'description'], + type: ['typeA', 'typeB'], + }); + }); + + it('converts the saved objects to results', async () => { + context.core.savedObjects.client.find.mockResolvedValue( + createFindResponse([ + createObject({ id: 'resultA', type: 'typeA', score: 50 }, { title: 'titleA' }), + createObject({ id: 'resultB', type: 'typeB', score: 78 }, { description: 'titleB' }), + ]) + ); + + const results = await provider.find('term', defaultOption, context).toPromise(); + expect(results).toEqual([ + { + id: 'resultA', + title: 'titleA', + type: 'typeA', + url: '/type-a/resultA', + score: 50, + }, + { + id: 'resultB', + title: 'titleB', + type: 'typeB', + url: '/type-b/resultB', + score: 78, + }, + ]); + }); + + it('only emits results until `aborted$` emits', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + // test scheduler doesnt play well with promises. need to workaround by passing + // an observable instead. Behavior with promise is asserted in previous tests of the suite + context.core.savedObjects.client.find.mockReturnValue( + hot('---a', { a: createFindResponse([]) }) as any + ); + + const resultObs = provider.find( + 'term', + { ...defaultOption, aborted$: hot('-(a|)', { a: undefined }) }, + context + ); + + expectObservable(resultObs).toBe('-|'); + }); + }); +}); diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts new file mode 100644 index 000000000000..b423b19ebc67 --- /dev/null +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { from } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { GlobalSearchResultProvider } from '../../../../global_search/server'; +import { mapToResults } from './map_object_to_result'; + +export const createSavedObjectsResultProvider = (): GlobalSearchResultProvider => { + return { + id: 'savedObjects', + find: (term, { aborted$, maxResults, preference }, { core }) => { + const { typeRegistry, client } = core.savedObjects; + + const searchableTypes = typeRegistry + .getVisibleTypes() + .filter((type) => type.management?.defaultSearchField && type.management?.getInAppUrl); + const searchFields = uniq( + searchableTypes.map((type) => type.management!.defaultSearchField!) + ); + + const responsePromise = client.find({ + page: 1, + perPage: maxResults, + search: term, + preference, + searchFields, + type: searchableTypes.map((type) => type.name), + }); + + return from(responsePromise).pipe( + takeUntil(aborted$), + map((res) => mapToResults(res.saved_objects, typeRegistry)) + ); + }, + }; +}; + +const uniq = (values: T[]): T[] => [...new Set(values)]; diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 648010eeeafe..b0b8cf14ff69 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 e7afc8f12859..a1eac5264bb6 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 98bd3077670a..5eb4eaf6e2ca 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 2ff3743cd866..fb3e16e5345c 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 07a27e2414ae..69d7a13edfcf 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 526b9fede2a6..d1700f0e611c 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 7696b3832c51..000000000000 --- 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 4ad428744dea..119d4e0c54ed 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 4e76a40ced52..6b1005b4faa0 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 249881f668d9..608a8b8aca29 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 006a2d9dea8f..14318b5fa2a8 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 05a5ed462d8f..f9e6234e1415 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 000000000000..51e8a829e81b --- /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 000000000000..64c7cd400ba0 --- /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 000000000000..0c64c38c8963 --- /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 000000000000..b454d8697c5f --- /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 000000000000..ad75c8dcbcc5 --- /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 000000000000..0a305eec1918 --- /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 000000000000..6abbbe65790e --- /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 000000000000..af48c3c79379 --- /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 000000000000..941e8ec362de --- /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 000000000000..7236a385a704 --- /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/index_management/public/application/components/component_templates/component_template_selector/components/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/index.ts new file mode 100644 index 000000000000..999b2e64cf13 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './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 000000000000..261a3d50d462 --- /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 c78d24f126e2..bfea8d39e120 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 72e79a57ae41..52235502e33d 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 4a8cf965adfb..63fe127c6b2d 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 97ffa4d875ec..27ee2bb81caf 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 4e56f4a8c981..bd19c2004894 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 b67a9c355e72..b0a76b828449 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 0d28ec4b50c9..d71d72d873c8 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 a5953ea00a10..c8297e6f298b 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 2b9b689e17cb..bbf7a04080a2 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 34e05d88c651..38c4a85bbe0f 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 c540ddceb95c..42be2c4b28c1 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 897e86c99eca..9b0eeb7d18f6 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 b7e3e36e6181..d8baca2db78a 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 000000000000..01771f40f89e --- /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 000000000000..b9b09bf0e3d9 --- /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 d011b4b06546..44ec4db0873f 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 867ecff79985..68a341949908 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 7f301b0a9c28..880c7fbd7f23 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 8a2c991aea8d..269ad9425107 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 9ff73b71adf5..5af3b4dd00c4 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 1931884cf730..5c249ee474b0 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 dcaba319bb21..156d792c26f1 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 ab4ce6a61a9b..f85b14ea0d2d 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 edce05018ce3..99915c2b70e2 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 7c3f8c07a7e0..6a5328f76fb0 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 f567b9835d53..fb82f52968eb 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 2a895196189d..8831fa2368f4 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 0fbf8ea5036c..02686c4f7d6f 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 69cd07ba6dba..ad221ae73fec 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 6c0fbe3dd6a6..9f8bce241ae6 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 000000000000..fc5719cc04d0 --- /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 e0d92b380078..4b735c941be7 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 ae5f7802a840..1d8645268dc2 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 7e9c3174d059..3055321d6b59 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 6ab28e902112..f82ea8f3cf15 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/common/constants.ts b/x-pack/plugins/infra/common/constants.ts new file mode 100644 index 000000000000..65dcb2e43c6f --- /dev/null +++ b/x-pack/plugins/infra/common/constants.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 const DEFAULT_SOURCE_ID = 'default'; diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts index 15615046bdd6..30b6be435837 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts @@ -8,3 +8,4 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; export * from './log_entry_category_examples'; export * from './log_entry_rate'; +export * from './log_entry_rate_examples'; diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts index dfc3d2aabd11..b7e8a4973515 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts @@ -30,6 +30,7 @@ export type GetLogEntryRateRequestPayload = rt.TypeOf; + export const logEntryRatePartitionRT = rt.type({ analysisBucketCount: rt.number, anomalies: rt.array(logEntryRateAnomalyRT), diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate_examples.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate_examples.ts new file mode 100644 index 000000000000..700f87ec3beb --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate_examples.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +import { + badRequestErrorRT, + forbiddenErrorRT, + timeRangeRT, + routeTimingMetadataRT, +} from '../../shared'; + +export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH = + '/api/infra/log_analysis/results/log_entry_rate_examples'; + +/** + * request + */ + +export const getLogEntryRateExamplesRequestPayloadRT = rt.type({ + data: rt.type({ + // the dataset to fetch the log rate examples from + dataset: rt.string, + // the number of examples to fetch + exampleCount: rt.number, + // the id of the source configuration + sourceId: rt.string, + // the time range to fetch the log rate examples from + timeRange: timeRangeRT, + }), +}); + +export type GetLogEntryRateExamplesRequestPayload = rt.TypeOf< + typeof getLogEntryRateExamplesRequestPayloadRT +>; + +/** + * response + */ + +const logEntryRateExampleRT = rt.type({ + id: rt.string, + dataset: rt.string, + message: rt.string, + timestamp: rt.number, + tiebreaker: rt.number, +}); + +export type LogEntryRateExample = rt.TypeOf; + +export const getLogEntryRateExamplesSuccessReponsePayloadRT = rt.intersection([ + rt.type({ + data: rt.type({ + examples: rt.array(logEntryRateExampleRT), + }), + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetLogEntryRateExamplesSuccessReponsePayload = rt.TypeOf< + typeof getLogEntryRateExamplesSuccessReponsePayloadRT +>; + +export const getLogEntryRateExamplesResponsePayloadRT = rt.union([ + getLogEntryRateExamplesSuccessReponsePayloadRT, + badRequestErrorRT, + forbiddenErrorRT, +]); + +export type GetLogEntryRateExamplesResponsePayload = rt.TypeOf< + typeof getLogEntryRateExamplesResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts index f0aa2067a24c..b8fba7a14e24 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts @@ -35,7 +35,7 @@ export type SetupStatus = | { type: 'skipped'; newlyCreated?: boolean; - }; // setup is hidden + }; // setup is not necessary /** * Maps a job status to the possibility that results have already been produced 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 000000000000..99ab129fc36e --- /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 d0b4045949d3..8d36262b5579 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 0e631b1e333d..f46a7f3e5a5e 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/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx similarity index 95% rename from x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator.tsx rename to x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx index e50231316fb5..e85145b83a30 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx @@ -10,7 +10,7 @@ import { formatAnomalyScore, getSeverityCategoryForScore, ML_SEVERITY_COLORS, -} from '../../../../../../common/log_analysis'; +} from '../../../../common/log_analysis'; export const AnomalySeverityIndicator: React.FunctionComponent<{ anomalyScore: number; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples.tsx new file mode 100644 index 000000000000..2ec9922d9455 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { euiStyled } from '../../../../../observability/public'; +import { LogEntryExampleMessagesEmptyIndicator } from './log_entry_examples_empty_indicator'; +import { LogEntryExampleMessagesFailureIndicator } from './log_entry_examples_failure_indicator'; +import { LogEntryExampleMessagesLoadingIndicator } from './log_entry_examples_loading_indicator'; + +interface Props { + isLoading: boolean; + hasFailedLoading: boolean; + hasResults: boolean; + exampleCount: number; + onReload: () => void; +} +export const LogEntryExampleMessages: React.FunctionComponent = ({ + isLoading, + hasFailedLoading, + exampleCount, + hasResults, + onReload, + children, +}) => { + return ( + + {isLoading ? ( + + ) : hasFailedLoading ? ( + + ) : !hasResults ? ( + + ) : ( + children + )} + + ); +}; + +const Wrapper = euiStyled.div` + align-items: stretch; + flex-direction: column; + flex: 1 0 0%; + overflow: hidden; +`; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_empty_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_empty_indicator.tsx similarity index 81% rename from x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_empty_indicator.tsx rename to x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_empty_indicator.tsx index ac572a5f6cf2..1d6028ed032a 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_empty_indicator.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_empty_indicator.tsx @@ -7,20 +7,20 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -export const CategoryExampleMessagesEmptyIndicator: React.FunctionComponent<{ +export const LogEntryExampleMessagesEmptyIndicator: React.FunctionComponent<{ onReload: () => void; }> = ({ onReload }) => ( diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_failure_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_failure_indicator.tsx similarity index 75% rename from x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_failure_indicator.tsx rename to x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_failure_indicator.tsx index 7865dcd0226e..dca786bce3b7 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_failure_indicator.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_failure_indicator.tsx @@ -7,22 +7,22 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -export const CategoryExampleMessagesFailureIndicator: React.FunctionComponent<{ +export const LogEntryExampleMessagesFailureIndicator: React.FunctionComponent<{ onRetry: () => void; }> = ({ onRetry }) => ( diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_loading_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_loading_indicator.tsx similarity index 89% rename from x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_loading_indicator.tsx rename to x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_loading_indicator.tsx index cad87a96a132..8217b6ef8096 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_messages_loading_indicator.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_examples/log_entry_examples_loading_indicator.tsx @@ -7,7 +7,7 @@ import { EuiLoadingContent } from '@elastic/eui'; import React from 'react'; -export const CategoryExampleMessagesLoadingIndicator: React.FunctionComponent<{ +export const LogEntryExampleMessagesLoadingIndicator: React.FunctionComponent<{ exampleCount: number; }> = ({ exampleCount }) => ( <> diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index bc592c71898b..c50a82006941 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -68,7 +68,7 @@ export const LogColumnHeaders: React.FunctionComponent<{ ); }; -const LogColumnHeader: React.FunctionComponent<{ +export const LogColumnHeader: React.FunctionComponent<{ columnWidth: LogEntryColumnWidth; 'data-test-subj'?: string; }> = ({ children, columnWidth, 'data-test-subj': dataTestSubj }) => ( @@ -77,7 +77,7 @@ const LogColumnHeader: React.FunctionComponent<{ ); -const LogColumnHeadersWrapper = euiStyled.div.attrs((props) => ({ +export const LogColumnHeadersWrapper = euiStyled.div.attrs((props) => ({ role: props.role ?? 'row', }))` align-items: stretch; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts b/x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts index dbf162171cac..bc687baf7c46 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LogEntryColumn, LogEntryColumnWidths, useColumnWidths } from './log_entry_column'; +export { + LogEntryColumn, + LogEntryColumnWidths, + useColumnWidths, + iconColumnId, +} from './log_entry_column'; export { LogEntryFieldColumn } from './log_entry_field_column'; export { LogEntryMessageColumn } from './log_entry_message_column'; export { LogEntryRowWrapper } from './log_entry_row'; export { LogEntryTimestampColumn } from './log_entry_timestamp_column'; export { ScrollableLogTextStreamView } from './scrollable_log_text_stream_view'; +export { LogEntryContextMenu } from './log_entry_context_menu'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx index 4aa81846d90e..adc1ce4d8c9f 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx @@ -13,7 +13,8 @@ import { LogEntryColumnContent } from './log_entry_column'; interface LogEntryContextMenuItem { label: string; - onClick: () => void; + onClick: (e: React.MouseEvent) => void; + href?: string; } interface LogEntryContextMenuProps { @@ -40,9 +41,9 @@ export const LogEntryContextMenu: React.FC = ({ }) => { const closeMenuAndCall = useMemo(() => { return (callback: LogEntryContextMenuItem['onClick']) => { - return () => { + return (e: React.MouseEvent) => { onClose(); - callback(); + callback(e); }; }; }, [onClose]); @@ -60,7 +61,7 @@ export const LogEntryContextMenu: React.FC = ({ const wrappedItems = useMemo(() => { return items.map((item, i) => ( - + {item.label} )); diff --git a/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx b/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx deleted file mode 100644 index 9f55126a1440..000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx +++ /dev/null @@ -1,161 +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 { EuiBadge, EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; -import { EuiSelectableOption } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; - -import { LogColumnConfiguration } from '../../utils/source_configuration'; -import { useVisibilityState } from '../../utils/use_visibility_state'; -import { euiStyled } from '../../../../observability/public'; - -interface SelectableColumnOption { - optionProps: EuiSelectableOption; - columnConfiguration: LogColumnConfiguration; -} - -export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ - addLogColumn: (logColumnConfiguration: LogColumnConfiguration) => void; - availableFields: string[]; - isDisabled?: boolean; -}> = ({ addLogColumn, availableFields, isDisabled }) => { - const { isVisible: isOpen, show: openPopover, hide: closePopover } = useVisibilityState(false); - - const availableColumnOptions = useMemo( - () => [ - { - optionProps: { - append: , - 'data-test-subj': 'addTimestampLogColumn', - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with field names - key: 'timestamp', - label: 'Timestamp', - }, - columnConfiguration: { - timestampColumn: { - id: uuidv4(), - }, - }, - }, - { - optionProps: { - 'data-test-subj': 'addMessageLogColumn', - append: , - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with field names - key: 'message', - label: 'Message', - }, - columnConfiguration: { - messageColumn: { - id: uuidv4(), - }, - }, - }, - ...availableFields.map((field) => ({ - optionProps: { - 'data-test-subj': `addFieldLogColumn addFieldLogColumn:${field}`, - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with fields that only differ in the - // case (e.g. the metricbeat mongodb module) - key: `field-${field}`, - label: field, - }, - columnConfiguration: { - fieldColumn: { - id: uuidv4(), - field, - }, - }, - })), - ], - [availableFields] - ); - - const availableOptions = useMemo( - () => availableColumnOptions.map((availableColumnOption) => availableColumnOption.optionProps), - [availableColumnOptions] - ); - - const handleColumnSelection = useCallback( - (selectedOptions: EuiSelectableOption[]) => { - closePopover(); - - const selectedOptionIndex = selectedOptions.findIndex( - (selectedOption) => selectedOption.checked === 'on' - ); - const selectedOption = availableColumnOptions[selectedOptionIndex]; - - addLogColumn(selectedOption.columnConfiguration); - }, - [addLogColumn, availableColumnOptions, closePopover] - ); - - return ( - - - - } - closePopover={closePopover} - id="addLogColumn" - isOpen={isOpen} - ownFocus - panelPaddingSize="none" - > - - {(list, search) => ( - - {search} - {list} - - )} - - - ); -}; - -const searchProps = { - 'data-test-subj': 'fieldSearchInput', -}; - -const selectableListProps = { - showIcons: false, -}; - -const SystemColumnBadge: React.FunctionComponent = () => ( - - - -); - -const SelectableContent = euiStyled.div` - width: 400px; -`; diff --git a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index 369f07be67bf..5ad05deafd69 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -27,9 +27,7 @@ interface FieldsConfigurationPanelProps { isLoading: boolean; readOnly: boolean; podFieldProps: InputFieldProps; - tiebreakerFieldProps: InputFieldProps; timestampFieldProps: InputFieldProps; - displaySettings: 'metrics' | 'logs'; } export const FieldsConfigurationPanel = ({ @@ -38,15 +36,12 @@ export const FieldsConfigurationPanel = ({ isLoading, readOnly, podFieldProps, - tiebreakerFieldProps, timestampFieldProps, - displaySettings, }: FieldsConfigurationPanelProps) => { const isHostValueDefault = hostFieldProps.value === 'host.name'; const isContainerValueDefault = containerFieldProps.value === 'container.id'; const isPodValueDefault = podFieldProps.value === 'kubernetes.pod.uid'; const isTimestampValueDefault = timestampFieldProps.value === '@timestamp'; - const isTiebreakerValueDefault = tiebreakerFieldProps.value === '_doc'; return ( @@ -139,194 +134,141 @@ export const FieldsConfigurationPanel = ({ /> - {displaySettings === 'logs' && ( - <> - - - - } - description={ - - } - > - _doc, - }} - /> - } - isInvalid={tiebreakerFieldProps.isInvalid} - label={ - - } - > - - - - - )} - {displaySettings === 'metrics' && ( - <> - - - - } - description={ - - } - > - container.id, - }} - /> - } - isInvalid={containerFieldProps.isInvalid} - label={ - - } - > - - - - - - - } - description={ - - } - > - host.name, - }} - /> - } - isInvalid={hostFieldProps.isInvalid} - label={ - - } - > - - - - - - - } - description={ - - } - > - kubernetes.pod.uid, - }} - /> - } - isInvalid={podFieldProps.isInvalid} - label={ - - } - > - - - - - )} + + + + } + description={ + + } + > + container.id, + }} + /> + } + isInvalid={containerFieldProps.isInvalid} + label={ + + } + > + + + + + + + } + description={ + + } + > + host.name, + }} + /> + } + isInvalid={hostFieldProps.isInvalid} + label={ + + } + > + + + + + + + } + description={ + + } + > + kubernetes.pod.uid, + }} + /> + } + isInvalid={podFieldProps.isInvalid} + label={ + + } + > + + + ); }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index 1d634b781bd3..e9817331ace9 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -21,17 +21,13 @@ import { InputFieldProps } from './input_fields'; interface IndicesConfigurationPanelProps { isLoading: boolean; readOnly: boolean; - logAliasFieldProps: InputFieldProps; metricAliasFieldProps: InputFieldProps; - displaySettings: 'metrics' | 'logs'; } export const IndicesConfigurationPanel = ({ isLoading, readOnly, - logAliasFieldProps, metricAliasFieldProps, - displaySettings, }: IndicesConfigurationPanelProps) => ( @@ -43,101 +39,51 @@ export const IndicesConfigurationPanel = ({ - {displaySettings === 'metrics' && ( - - - - } - description={ + - } - > - metricbeat-*, - }} - /> - } - isInvalid={metricAliasFieldProps.isInvalid} - label={ - - } - > - + } + description={ + + } + > + metrics-*,metricbeat-*, + }} /> - - - )} - {displaySettings === 'logs' && ( - - - } - description={ + isInvalid={metricAliasFieldProps.isInvalid} + label={ } > - filebeat-*, - }} - /> - } - isInvalid={logAliasFieldProps.isInvalid} - label={ - - } - > - - - - )} + disabled={isLoading} + readOnly={readOnly} + isLoading={isLoading} + {...metricAliasFieldProps} + /> + + ); diff --git a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx deleted file mode 100644 index 46ab1e65c29d..000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx +++ /dev/null @@ -1,279 +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 { - EuiButtonIcon, - EuiEmptyPrompt, - EuiForm, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiDragDropContext, - EuiDraggable, - EuiDroppable, - EuiIcon, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback } from 'react'; -import { DragHandleProps, DropResult } from '../../../../observability/public'; - -import { AddLogColumnButtonAndPopover } from './add_log_column_popover'; -import { - FieldLogColumnConfigurationProps, - LogColumnConfigurationProps, -} from './log_columns_configuration_form_state'; -import { LogColumnConfiguration } from '../../utils/source_configuration'; - -interface LogColumnsConfigurationPanelProps { - availableFields: string[]; - isLoading: boolean; - logColumnConfiguration: LogColumnConfigurationProps[]; - addLogColumn: (logColumn: LogColumnConfiguration) => void; - moveLogColumn: (sourceIndex: number, destinationIndex: number) => void; -} - -export const LogColumnsConfigurationPanel: React.FunctionComponent = ({ - addLogColumn, - moveLogColumn, - availableFields, - isLoading, - logColumnConfiguration, -}) => { - const onDragEnd = useCallback( - ({ source, destination }: DropResult) => - destination && moveLogColumn(source.index, destination.index), - [moveLogColumn] - ); - - return ( - - - - -

- -

-
-
- - - -
- {logColumnConfiguration.length > 0 ? ( - - - <> - {/* Fragment here necessary for typechecking */} - {logColumnConfiguration.map((column, index) => ( - - {(provided) => ( - - )} - - ))} - - - - ) : ( - - )} -
- ); -}; - -interface LogColumnConfigurationPanelProps { - logColumnConfigurationProps: LogColumnConfigurationProps; - dragHandleProps: DragHandleProps; -} - -const LogColumnConfigurationPanel: React.FunctionComponent = ( - props -) => ( - <> - - {props.logColumnConfigurationProps.type === 'timestamp' ? ( - - ) : props.logColumnConfigurationProps.type === 'message' ? ( - - ) : ( - - )} - -); - -const TimestampLogColumnConfigurationPanel: React.FunctionComponent = ({ - logColumnConfigurationProps, - dragHandleProps, -}) => ( - timestamp, - }} - /> - } - removeColumn={logColumnConfigurationProps.remove} - dragHandleProps={dragHandleProps} - /> -); - -const MessageLogColumnConfigurationPanel: React.FunctionComponent = ({ - logColumnConfigurationProps, - dragHandleProps, -}) => ( - - } - removeColumn={logColumnConfigurationProps.remove} - dragHandleProps={dragHandleProps} - /> -); - -const FieldLogColumnConfigurationPanel: React.FunctionComponent<{ - logColumnConfigurationProps: FieldLogColumnConfigurationProps; - dragHandleProps: DragHandleProps; -}> = ({ - logColumnConfigurationProps: { - logColumnConfiguration: { field }, - remove, - }, - dragHandleProps, -}) => { - const fieldLogColumnTitle = i18n.translate( - 'xpack.infra.sourceConfiguration.fieldLogColumnTitle', - { - defaultMessage: 'Field', - } - ); - return ( - - - -
- -
-
- {fieldLogColumnTitle} - - {field} - - - - -
-
- ); -}; - -const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ - fieldName: React.ReactNode; - helpText: React.ReactNode; - removeColumn: () => void; - dragHandleProps: DragHandleProps; -}> = ({ fieldName, helpText, removeColumn, dragHandleProps }) => ( - - - -
- -
-
- {fieldName} - - - {helpText} - - - - - -
-
-); - -const RemoveLogColumnButton: React.FunctionComponent<{ - onClick?: () => void; - columnDescription: string; -}> = ({ onClick, columnDescription }) => { - const removeColumnLabel = i18n.translate( - 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel', - { - defaultMessage: 'Remove {columnDescription} column', - values: { columnDescription }, - } - ); - - return ( - - ); -}; - -const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => ( - - - - } - body={ -

- -

- } - /> -); diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 43bdc1f4cedc..53b62f8dda04 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -22,19 +22,16 @@ import { Source } from '../../containers/source'; import { FieldsConfigurationPanel } from './fields_configuration_panel'; import { IndicesConfigurationPanel } from './indices_configuration_panel'; import { NameConfigurationPanel } from './name_configuration_panel'; -import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; import { SourceLoadingPage } from '../source_loading_page'; import { Prompt } from '../../utils/navigation_warning_prompt'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; - displaySettings: 'metrics' | 'logs'; } export const SourceConfigurationSettings = ({ shouldAllowEdit, - displaySettings, }: SourceConfigurationSettingsProps) => { const { createSourceConfiguration, @@ -45,16 +42,8 @@ export const SourceConfigurationSettings = ({ updateSourceConfiguration, } = useContext(Source.Context); - const availableFields = useMemo( - () => (source && source.status ? source.status.indexFields.map((field) => field.name) : []), - [source] - ); - const { - addLogColumn, - moveLogColumn, indicesConfigurationProps, - logColumnConfigurationProps, errors, resetForm, isFormDirty, @@ -119,10 +108,8 @@ export const SourceConfigurationSettings = ({ @@ -133,23 +120,10 @@ export const SourceConfigurationSettings = ({ isLoading={isLoading} podFieldProps={indicesConfigurationProps.podField} readOnly={!isWriteable} - tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField} timestampFieldProps={indicesConfigurationProps.timestampField} - displaySettings={displaySettings} /> - {displaySettings === 'logs' && ( - - - - )} {errors.length > 0 ? ( <> diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx index 10205e9684ef..a0046b630bfe 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx @@ -252,7 +252,7 @@ const getSetupStatus = (everyJobStatus: Record(everyJobStatus).reduce((setupStatus, [, jobStatus]) => { if (jobStatus === 'missing') { return { type: 'required', reason: 'missing' }; - } else if (setupStatus.type === 'required') { + } else if (setupStatus.type === 'required' || setupStatus.type === 'succeeded') { return setupStatus; } else if (setupStatus.type === 'skipped' || isJobStatusWithResults(jobStatus)) { return { diff --git a/x-pack/plugins/infra/public/index.scss b/x-pack/plugins/infra/public/index.scss index 05e045c1bd53..a3d74e3afebe 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 000000000000..21946c7c5653 --- /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 000000000000..d10ad5dda532 --- /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 cc4b6967d34f..c2b49c43281a 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 10320ebbe760..37203084124f 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/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index 5d9adb8a4f6e..26633cd190a0 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { isSetupStatusWithResults } from '../../../../common/log_analysis'; +import React, { useEffect, useState, useCallback } from 'react'; +import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, @@ -21,6 +21,7 @@ import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryCategoriesResultsContent } from './page_results_content'; import { LogEntryCategoriesSetupContent } from './page_setup_content'; import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module'; +import { LogEntryCategoriesSetupFlyout } from './setup_flyout'; export const LogEntryCategoriesPageContent = () => { const { @@ -37,7 +38,11 @@ export const LogEntryCategoriesPageContent = () => { hasLogAnalysisSetupCapabilities, } = useLogAnalysisCapabilitiesContext(); - const { fetchJobStatus, setupStatus } = useLogEntryCategoriesModuleContext(); + const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryCategoriesModuleContext(); + + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const openFlyout = useCallback(() => setIsFlyoutOpen(true), []); + const closeFlyout = useCallback(() => setIsFlyoutOpen(false), []); useEffect(() => { if (hasLogAnalysisReadCapabilities) { @@ -45,6 +50,13 @@ export const LogEntryCategoriesPageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); + // Open flyout if there are no ML jobs + useEffect(() => { + if (setupStatus.type === 'required' && setupStatus.reason === 'missing') { + openFlyout(); + } + }, [setupStatus, openFlyout]); + if (isLoading || isUninitialized) { return ; } else if (hasFailedLoadingSource) { @@ -63,11 +75,21 @@ export const LogEntryCategoriesPageContent = () => { ); } else if (setupStatus.type === 'unknown') { return ; - } else if (isSetupStatusWithResults(setupStatus)) { - return ; + } else if (isJobStatusWithResults(jobStatus['log-entry-categories-count'])) { + return ( + <> + + + + ); } else if (!hasLogAnalysisSetupCapabilities) { return ; } else { - return ; + return ( + <> + + + + ); } }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index a00351551e2d..b4c044fe1cfc 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -24,7 +24,13 @@ import { const JOB_STATUS_POLLING_INTERVAL = 30000; -export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { +interface LogEntryCategoriesResultsContentProps { + onOpenSetup: () => void; +} + +export const LogEntryCategoriesResultsContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results', delay: 15000 }); @@ -123,12 +129,25 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { [setAutoRefresh] ); + const viewSetupFlyoutForReconfiguration = useCallback(() => { + viewSetupForReconfiguration(); + onOpenSetup(); + }, [onOpenSetup, viewSetupForReconfiguration]); + + const viewSetupFlyoutForUpdate = useCallback(() => { + viewSetupForUpdate(); + onOpenSetup(); + }, [onOpenSetup, viewSetupForUpdate]); + const hasResults = useMemo(() => topLogEntryCategories.length > 0, [ topLogEntryCategories.length, ]); const isFirstUse = useMemo( - () => setupStatus.type === 'skipped' && !!setupStatus.newlyCreated && !hasResults, + () => + ((setupStatus.type === 'skipped' && !!setupStatus.newlyCreated) || + setupStatus.type === 'succeeded') && + !hasResults, [hasResults, setupStatus] ); @@ -184,8 +203,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { hasOutdatedJobDefinitions={hasOutdatedJobDefinitions} hasStoppedJobs={hasStoppedJobs} isFirstUse={isFirstUse} - onRecreateMlJobForReconfiguration={viewSetupForReconfiguration} - onRecreateMlJobForUpdate={viewSetupForUpdate} + onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration} + onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate} qualityWarnings={categoryQualityWarnings} />
@@ -197,7 +216,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { isLoadingTopCategories={isLoadingTopLogEntryCategories} jobId={jobIds['log-entry-categories-count']} onChangeDatasetSelection={setCategoryQueryDatasets} - onRequestRecreateMlJob={viewSetupForReconfiguration} + onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration} selectedDatasets={categoryQueryDatasets} sourceId={sourceId} timeRange={categoryQueryTimeRange.timeRange} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx index 7ae38234ae22..8d5d8a42200e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx @@ -4,98 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiSteps, EuiText } from '@elastic/eui'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; +import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; -import { BetaBadge } from '../../../components/beta_badge'; import { - createInitialConfigurationStep, - createProcessStep, LogAnalysisSetupPage, LogAnalysisSetupPageContent, LogAnalysisSetupPageHeader, } from '../../../components/logging/log_analysis_setup'; import { useTrackPageview } from '../../../../../observability/public'; -import { useLogEntryCategoriesSetup } from './use_log_entry_categories_setup'; -export const LogEntryCategoriesSetupContent: React.FunctionComponent = () => { +interface LogEntryCategoriesSetupContentProps { + onOpenSetup: () => void; +} + +export const LogEntryCategoriesSetupContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_setup' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_setup', delay: 15000 }); - const { - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setValidatedIndices, - setUp, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - } = useLogEntryCategoriesSetup(); - - const steps = useMemo( - () => [ - createInitialConfigurationStep({ - setStartTime, - setEndTime, - startTime, - endTime, - isValidating, - validatedIndices, - setupStatus, - setValidatedIndices, - validationErrors, - }), - createProcessStep({ - cleanUpAndSetUp, - errorMessages: lastSetupErrorMessages, - isConfigurationValid: validationErrors.length <= 0 && !isValidating, - setUp, - setupStatus, - viewResults, - }), - ], - [ - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setUp, - setValidatedIndices, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - ] - ); - return ( {' '} - + defaultMessage="Set up log category analysis" + /> - +

+ +

- + + +
); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx index dafaa37006be..47bb31ab4ae3 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/anomaly_severity_indicator_list.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { LogEntryCategoryDataset } from '../../../../../../common/http_api/log_analysis'; import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; -import { AnomalySeverityIndicator } from './anomaly_severity_indicator'; +import { AnomalySeverityIndicator } from '../../../../../components/logging/log_analysis_results/anomaly_severity_indicator'; export const AnomalySeverityIndicatorList: React.FunctionComponent<{ datasets: LogEntryCategoryDataset[]; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx index c0728c0a5548..d939d6738c53 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx @@ -5,14 +5,10 @@ */ import React, { useEffect } from 'react'; - -import { euiStyled } from '../../../../../../../observability/public'; -import { TimeRange } from '../../../../../../common/http_api/shared'; import { useLogEntryCategoryExamples } from '../../use_log_entry_category_examples'; +import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples'; +import { TimeRange } from '../../../../../../common/http_api/shared'; import { CategoryExampleMessage } from './category_example_message'; -import { CategoryExampleMessagesEmptyIndicator } from './category_example_messages_empty_indicator'; -import { CategoryExampleMessagesFailureIndicator } from './category_example_messages_failure_indicator'; -import { CategoryExampleMessagesLoadingIndicator } from './category_example_messages_loading_indicator'; const exampleCount = 5; @@ -39,30 +35,21 @@ export const CategoryDetailsRow: React.FunctionComponent<{ }, [getLogEntryCategoryExamples]); return ( - - {isLoadingLogEntryCategoryExamples ? ( - - ) : hasFailedLoadingLogEntryCategoryExamples ? ( - - ) : logEntryCategoryExamples.length === 0 ? ( - - ) : ( - logEntryCategoryExamples.map((categoryExample, categoryExampleIndex) => ( - - )) - )} - + 0} + exampleCount={exampleCount} + onReload={getLogEntryCategoryExamples} + > + {logEntryCategoryExamples.map((example, exampleIndex) => ( + + ))} + ); }; - -const CategoryExampleMessages = euiStyled.div` - align-items: stretch; - flex-direction: column; - flex: 1 0 0%; - overflow: hidden; -`; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/setup_flyout.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/setup_flyout.tsx new file mode 100644 index 000000000000..ab5eae1ab300 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/setup_flyout.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiText, + EuiSpacer, + EuiSteps, +} from '@elastic/eui'; + +import { + createInitialConfigurationStep, + createProcessStep, +} from '../../../components/logging/log_analysis_setup'; +import { useLogEntryCategoriesSetup } from './use_log_entry_categories_setup'; + +interface LogEntryCategoriesSetupFlyoutProps { + isOpen: boolean; + onClose: () => void; +} + +export const LogEntryCategoriesSetupFlyout: React.FC = ({ + isOpen, + onClose, +}) => { + const { + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setValidatedIndices, + setUp, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResults, + } = useLogEntryCategoriesSetup(); + + const viewResultsAndClose = useCallback(() => { + viewResults(); + onClose(); + }, [viewResults, onClose]); + + const steps = useMemo( + () => [ + createInitialConfigurationStep({ + setStartTime, + setEndTime, + startTime, + endTime, + isValidating, + validatedIndices, + setupStatus, + setValidatedIndices, + validationErrors, + }), + createProcessStep({ + cleanUpAndSetUp, + errorMessages: lastSetupErrorMessages, + isConfigurationValid: validationErrors.length <= 0 && !isValidating, + setUp, + setupStatus, + viewResults: viewResultsAndClose, + }), + ], + [ + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setUp, + setValidatedIndices, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResultsAndClose, + ] + ); + + if (!isOpen) { + return null; + } + return ( + + + +

+ +

+
+
+ + +

+ +

+
+ + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 4ec05a977851..012b694bdbd2 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { isSetupStatusWithResults } from '../../../../common/log_analysis'; +import React, { useEffect, useState, useCallback } from 'react'; +import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, @@ -21,6 +21,7 @@ import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryRateResultsContent } from './page_results_content'; import { LogEntryRateSetupContent } from './page_setup_content'; import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; +import { LogEntryRateSetupFlyout } from './setup_flyout'; export const LogEntryRatePageContent = () => { const { @@ -37,7 +38,11 @@ export const LogEntryRatePageContent = () => { hasLogAnalysisSetupCapabilities, } = useLogAnalysisCapabilitiesContext(); - const { fetchJobStatus, setupStatus } = useLogEntryRateModuleContext(); + const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryRateModuleContext(); + + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const openFlyout = useCallback(() => setIsFlyoutOpen(true), []); + const closeFlyout = useCallback(() => setIsFlyoutOpen(false), []); useEffect(() => { if (hasLogAnalysisReadCapabilities) { @@ -45,6 +50,13 @@ export const LogEntryRatePageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); + // Open flyout if there are no ML jobs + useEffect(() => { + if (setupStatus.type === 'required' && setupStatus.reason === 'missing') { + openFlyout(); + } + }, [setupStatus, openFlyout]); + if (isLoading || isUninitialized) { return ; } else if (hasFailedLoadingSource) { @@ -63,11 +75,21 @@ export const LogEntryRatePageContent = () => { ); } else if (setupStatus.type === 'unknown') { return ; - } else if (isSetupStatusWithResults(setupStatus)) { - return ; + } else if (isJobStatusWithResults(jobStatus['log-entry-rate'])) { + return ( + <> + + + + ); } else if (!hasLogAnalysisSetupCapabilities) { return ; } else { - return ; + return ( + <> + + + + ); } }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 3c8db3f8246c..bf4dbcd87cc4 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -36,7 +36,13 @@ import { const JOB_STATUS_POLLING_INTERVAL = 30000; -export const LogEntryRateResultsContent: React.FunctionComponent = () => { +interface LogEntryRateResultsContentProps { + onOpenSetup: () => void; +} + +export const LogEntryRateResultsContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 }); @@ -127,13 +133,26 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { [setAutoRefresh] ); + const viewSetupFlyoutForReconfiguration = useCallback(() => { + viewSetupForReconfiguration(); + onOpenSetup(); + }, [viewSetupForReconfiguration, onOpenSetup]); + + const viewSetupFlyoutForUpdate = useCallback(() => { + viewSetupForUpdate(); + onOpenSetup(); + }, [viewSetupForUpdate, onOpenSetup]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [ logEntryRate, ]); const isFirstUse = useMemo( - () => setupStatus.type === 'skipped' && !!setupStatus.newlyCreated && !hasResults, + () => + ((setupStatus.type === 'skipped' && !!setupStatus.newlyCreated) || + setupStatus.type === 'succeeded') && + !hasResults, [hasResults, setupStatus] ); @@ -209,8 +228,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { hasOutdatedJobDefinitions={hasOutdatedJobDefinitions} hasStoppedJobs={hasStoppedJobs} isFirstUse={isFirstUse} - onRecreateMlJobForReconfiguration={viewSetupForReconfiguration} - onRecreateMlJobForUpdate={viewSetupForUpdate} + onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration} + onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate} />
@@ -227,7 +246,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { { +interface LogEntryRateSetupContentProps { + onOpenSetup: () => void; +} + +export const LogEntryRateSetupContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup', delay: 15000 }); - const { - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setValidatedIndices, - setUp, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - } = useLogEntryRateSetup(); - - const steps = useMemo( - () => [ - createInitialConfigurationStep({ - setStartTime, - setEndTime, - startTime, - endTime, - isValidating, - validatedIndices, - setupStatus, - setValidatedIndices, - validationErrors, - }), - createProcessStep({ - cleanUpAndSetUp, - errorMessages: lastSetupErrorMessages, - isConfigurationValid: validationErrors.length <= 0 && !isValidating, - setUp, - setupStatus, - viewResults, - }), - ], - [ - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setUp, - setValidatedIndices, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - ] - ); - return ( {' '} - + id="xpack.infra.logs.logEntryRate.setupTitle" + defaultMessage="Set up log anomaly analysis" + /> - +

+ +

- + + +
); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index a1d3d56beee2..c527b8c49d09 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -4,86 +4,129 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiStat } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; - +import React from 'react'; +import { useMount } from 'react-use'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; -import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; -import { LogEntryRateResults } from '../../use_log_entry_rate_results'; -import { - getAnnotationsForPartition, - getLogEntryRateSeriesForPartition, - getTotalNumberOfLogEntriesForPartition, -} from '../helpers/data_formatters'; -import { AnomaliesChart } from './chart'; +import { AnomalyRecord } from '../../use_log_entry_rate_results'; +import { useLogEntryRateModuleContext } from '../../use_log_entry_rate_module'; +import { useLogEntryRateExamples } from '../../use_log_entry_rate_examples'; +import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples'; +import { LogEntryRateExampleMessage, LogEntryRateExampleMessageHeaders } from './log_entry_example'; +import { euiStyled } from '../../../../../../../observability/public'; + +const EXAMPLE_COUNT = 5; + +const examplesTitle = i18n.translate('xpack.infra.logs.analysis.anomaliesTableExamplesTitle', { + defaultMessage: 'Example log entries', +}); export const AnomaliesTableExpandedRow: React.FunctionComponent<{ - partitionId: string; - results: LogEntryRateResults; - setTimeRange: (timeRange: TimeRange) => void; + anomaly: AnomalyRecord; timeRange: TimeRange; jobId: string; -}> = ({ results, timeRange, setTimeRange, partitionId, jobId }) => { - const logEntryRateSeries = useMemo( - () => - results?.histogramBuckets ? getLogEntryRateSeriesForPartition(results, partitionId) : [], - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - [results, partitionId] - ); - const anomalyAnnotations = useMemo( - () => - results?.histogramBuckets - ? getAnnotationsForPartition(results, partitionId) - : { - warning: [], - minor: [], - major: [], - critical: [], - }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - [results, partitionId] - ); - const totalNumberOfLogEntries = useMemo( - () => - results?.histogramBuckets - ? getTotalNumberOfLogEntriesForPartition(results, partitionId) - : undefined, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - [results, partitionId] - ); +}> = ({ anomaly, timeRange, jobId }) => { + const { + sourceConfiguration: { sourceId }, + } = useLogEntryRateModuleContext(); + + const { + getLogEntryRateExamples, + hasFailedLoadingLogEntryRateExamples, + isLoadingLogEntryRateExamples, + logEntryRateExamples, + } = useLogEntryRateExamples({ + dataset: anomaly.partitionId, + endTime: anomaly.startTime + anomaly.duration, + exampleCount: EXAMPLE_COUNT, + sourceId, + startTime: anomaly.startTime, + }); + + useMount(() => { + getLogEntryRateExamples(); + }); + return ( - - - - - - - - - - - - - - - + <> + + + +

{examplesTitle}

+
+ 0} + exampleCount={EXAMPLE_COUNT} + onReload={getLogEntryRateExamples} + > + {logEntryRateExamples.length > 0 ? ( + <> + + {logEntryRateExamples.map((example, exampleIndex) => ( + + ))} + + ) : null} + +
+ + + + + + + + + + +
+ ); }; + +const ExpandedContentWrapper = euiStyled(EuiFlexGroup)` + overflow: hidden; +`; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index 5ff3f318629f..a2d37455eac1 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -9,23 +9,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiStat, EuiTitle, EuiLoadingSpinner, } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; - import { euiStyled } from '../../../../../../../observability/public'; import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; -import { formatAnomalyScore } from '../../../../../../common/log_analysis'; -import { - getAnnotationsForAll, - getLogEntryRateCombinedSeries, - getTopAnomalyScoreAcrossAllPartitions, -} from '../helpers/data_formatters'; +import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters'; import { AnomaliesChart } from './chart'; import { AnomaliesTable } from './table'; import { RecreateJobButton } from '../../../../../components/logging/log_analysis_job_status'; @@ -67,14 +59,6 @@ export const AnomaliesResults: React.FunctionComponent<{ [results] ); - const topAnomalyScore = useMemo( - () => - results && results.histogramBuckets - ? getTopAnomalyScoreAcrossAllPartitions(results) - : undefined, - [results] - ); - return ( <> @@ -124,7 +108,7 @@ export const AnomaliesResults: React.FunctionComponent<{ ) : ( <> - + - - - - ; + interface ParsedAnnotationDetails { anomalyScoresByPartition: Array<{ partitionName: string; maximumAnomalyScore: number }>; } @@ -222,10 +189,3 @@ const renderAnnotationTooltip = (details?: string) => { const TooltipWrapper = euiStyled('div')` white-space: nowrap; `; - -const loadingAriaLabel = i18n.translate( - 'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel', - { defaultMessage: 'Loading anomalies' } -); - -const LoadingOverlayContent = () => ; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx new file mode 100644 index 000000000000..96f665b3693c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -0,0 +1,291 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useCallback, useState } from 'react'; +import moment from 'moment'; +import { encode } from 'rison-node'; +import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../observability/public'; +import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; +import { + LogEntryColumn, + LogEntryFieldColumn, + LogEntryMessageColumn, + LogEntryRowWrapper, + LogEntryTimestampColumn, + LogEntryContextMenu, + LogEntryColumnWidths, + iconColumnId, +} from '../../../../../components/logging/log_text_stream'; +import { + LogColumnHeadersWrapper, + LogColumnHeader, +} from '../../../../../components/logging/log_text_stream/column_headers'; +import { useLinkProps } from '../../../../../hooks/use_link_props'; +import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { partitionField } from '../../../../../../common/log_analysis/job_parameters'; +import { getEntitySpecificSingleMetricViewerLink } from '../../../../../components/logging/log_analysis_results/analyze_in_ml_button'; +import { LogEntryRateExample } from '../../../../../../common/http_api/log_analysis/results'; +import { + LogColumnConfiguration, + isTimestampLogColumnConfiguration, + isFieldLogColumnConfiguration, + isMessageLogColumnConfiguration, +} from '../../../../../utils/source_configuration'; +import { localizedDate } from '../../../../../../common/formatters/datetime'; + +export const exampleMessageScale = 'medium' as const; +export const exampleTimestampFormat = 'time' as const; + +const MENU_LABEL = i18n.translate('xpack.infra.logAnomalies.logEntryExamplesMenuLabel', { + defaultMessage: 'View actions for log entry', +}); + +const VIEW_IN_STREAM_LABEL = i18n.translate( + 'xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel', + { + defaultMessage: 'View in stream', + } +); + +const VIEW_ANOMALY_IN_ML_LABEL = i18n.translate( + 'xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel', + { + defaultMessage: 'View anomaly in machine learning', + } +); + +type Props = LogEntryRateExample & { + timeRange: TimeRange; + jobId: string; +}; + +export const LogEntryRateExampleMessage: React.FunctionComponent = ({ + id, + dataset, + message, + timestamp, + tiebreaker, + timeRange, + jobId, +}) => { + const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const openMenu = useCallback(() => setIsMenuOpen(true), []); + const closeMenu = useCallback(() => setIsMenuOpen(false), []); + const setItemIsHovered = useCallback(() => setIsHovered(true), []); + const setItemIsNotHovered = useCallback(() => setIsHovered(false), []); + + // the dataset must be encoded for the field column and the empty value must + // be turned into a user-friendly value + const encodedDatasetFieldValue = useMemo( + () => JSON.stringify(getFriendlyNameForPartitionId(dataset)), + [dataset] + ); + + const viewInStreamLinkProps = useLinkProps({ + app: 'logs', + pathname: 'stream', + search: { + logPosition: encode({ + end: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + position: { tiebreaker, time: timestamp }, + start: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + streamLive: false, + }), + flyoutOptions: encode({ + surroundingLogsId: id, + }), + logFilter: encode({ + expression: `${partitionField}: ${dataset}`, + kind: 'kuery', + }), + }, + }); + + const viewAnomalyInMachineLearningLinkProps = useLinkProps( + getEntitySpecificSingleMetricViewerLink(jobId, timeRange, { + [partitionField]: dataset, + }) + ); + + const menuItems = useMemo(() => { + if (!viewInStreamLinkProps.onClick || !viewAnomalyInMachineLearningLinkProps.onClick) { + return undefined; + } + + return [ + { + label: VIEW_IN_STREAM_LABEL, + onClick: viewInStreamLinkProps.onClick, + href: viewInStreamLinkProps.href, + }, + { + label: VIEW_ANOMALY_IN_ML_LABEL, + onClick: viewAnomalyInMachineLearningLinkProps.onClick, + href: viewAnomalyInMachineLearningLinkProps.href, + }, + ]; + }, [viewInStreamLinkProps, viewAnomalyInMachineLearningLinkProps]); + + return ( + + + + + + + + + + + + {(isHovered || isMenuOpen) && menuItems ? ( + + ) : null} + + + ); +}; + +const noHighlights: never[] = []; +const timestampColumnId = 'log-entry-example-timestamp-column' as const; +const messageColumnId = 'log-entry-examples-message-column' as const; +const datasetColumnId = 'log-entry-examples-dataset-column' as const; + +const DETAIL_FLYOUT_ICON_MIN_WIDTH = 32; +const COLUMN_PADDING = 8; + +export const columnWidths: LogEntryColumnWidths = { + [timestampColumnId]: { + growWeight: 0, + shrinkWeight: 0, + // w_score - w_padding = 130 px - 8 px + baseWidth: '122px', + }, + [messageColumnId]: { + growWeight: 1, + shrinkWeight: 0, + baseWidth: '0%', + }, + [datasetColumnId]: { + growWeight: 0, + shrinkWeight: 0, + baseWidth: '250px', + }, + [iconColumnId]: { + growWeight: 0, + shrinkWeight: 0, + baseWidth: `${DETAIL_FLYOUT_ICON_MIN_WIDTH + 2 * COLUMN_PADDING}px`, + }, +}; + +export const exampleMessageColumnConfigurations: LogColumnConfiguration[] = [ + { + timestampColumn: { + id: timestampColumnId, + }, + }, + { + messageColumn: { + id: messageColumnId, + }, + }, + { + fieldColumn: { + field: 'event.dataset', + id: datasetColumnId, + }, + }, +]; + +export const LogEntryRateExampleMessageHeaders: React.FunctionComponent<{ + dateTime: number; +}> = ({ dateTime }) => { + return ( + + <> + {exampleMessageColumnConfigurations.map((columnConfiguration) => { + if (isTimestampLogColumnConfiguration(columnConfiguration)) { + return ( + + {localizedDate(dateTime)} + + ); + } else if (isMessageLogColumnConfiguration(columnConfiguration)) { + return ( + + Message + + ); + } else if (isFieldLogColumnConfiguration(columnConfiguration)) { + return ( + + {columnConfiguration.fieldColumn.field} + + ); + } + })} + + {null} + + + + ); +}; + +const LogEntryRateExampleMessageHeadersWrapper = euiStyled(LogColumnHeadersWrapper)` + border-bottom: none; + box-shadow: none; + padding-right: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index a9090a90c0b9..c70a456bfe06 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -6,10 +6,10 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; +import moment from 'moment'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo, useState } from 'react'; import { useSet } from 'react-use'; -import { euiStyled } from '../../../../../../../observability/public'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { formatAnomalyScore, @@ -18,11 +18,16 @@ import { import { RowExpansionButton } from '../../../../../components/basic_table'; import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { AnomaliesTableExpandedRow } from './expanded_row'; +import { AnomalySeverityIndicator } from '../../../../../components/logging/log_analysis_results/anomaly_severity_indicator'; +import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; interface TableItem { - partitionName: string; - partitionId: string; - topAnomalyScore: number; + id: string; + dataset: string; + datasetName: string; + anomalyScore: number; + anomalyMessage: string; + startTime: number; } interface SortingOptions { @@ -32,73 +37,132 @@ interface SortingOptions { }; } -const partitionColumnName = i18n.translate( - 'xpack.infra.logs.analysis.anomaliesTablePartitionColumnName', +interface PaginationOptions { + pageIndex: number; + pageSize: number; + totalItemCount: number; + pageSizeOptions: number[]; + hidePerPageOptions: boolean; +} + +const anomalyScoreColumnName = i18n.translate( + 'xpack.infra.logs.analysis.anomaliesTableAnomalyScoreColumnName', + { + defaultMessage: 'Anomaly score', + } +); + +const anomalyMessageColumnName = i18n.translate( + 'xpack.infra.logs.analysis.anomaliesTableAnomalyMessageName', { - defaultMessage: 'Partition', + defaultMessage: 'Anomaly', } ); -const maxAnomalyScoreColumnName = i18n.translate( - 'xpack.infra.logs.analysis.anomaliesTableMaxAnomalyScoreColumnName', +const anomalyStartTimeColumnName = i18n.translate( + 'xpack.infra.logs.analysis.anomaliesTableAnomalyStartTime', { - defaultMessage: 'Max anomaly score', + defaultMessage: 'Start time', } ); +const datasetColumnName = i18n.translate( + 'xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName', + { + defaultMessage: 'Dataset', + } +); + +const moreThanExpectedAnomalyMessage = i18n.translate( + 'xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage', + { + defaultMessage: 'More log messages in this dataset than expected', + } +); + +const fewerThanExpectedAnomalyMessage = i18n.translate( + 'xpack.infra.logs.analysis.anomaliesTableFewerThanExpectedAnomalyMessage', + { + defaultMessage: 'Fewer log messages in this dataset than expected', + } +); + +const getAnomalyMessage = (actualRate: number, typicalRate: number): string => { + return actualRate < typicalRate + ? fewerThanExpectedAnomalyMessage + : moreThanExpectedAnomalyMessage; +}; + export const AnomaliesTable: React.FunctionComponent<{ results: LogEntryRateResults; setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; jobId: string; }> = ({ results, timeRange, setTimeRange, jobId }) => { + const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss'); + const tableItems: TableItem[] = useMemo(() => { - return Object.entries(results.partitionBuckets).map(([key, value]) => { + return results.anomalies.map((anomaly) => { return { - // The real ID - partitionId: key, - // Note: EUI's table expanded rows won't work with a key of '' in itemIdToExpandedRowMap, so we have to use the friendly name here - partitionName: getFriendlyNameForPartitionId(key), - topAnomalyScore: formatAnomalyScore(value.topAnomalyScore), + id: anomaly.id, + dataset: anomaly.partitionId, + datasetName: getFriendlyNameForPartitionId(anomaly.partitionId), + anomalyScore: formatAnomalyScore(anomaly.anomalyScore), + anomalyMessage: getAnomalyMessage(anomaly.actualLogEntryRate, anomaly.typicalLogEntryRate), + startTime: anomaly.startTime, }; }); }, [results]); - const [expandedDatasetIds, { add: expandDataset, remove: collapseDataset }] = useSet( - new Set() - ); + const [expandedIds, { add: expandId, remove: collapseId }] = useSet(new Set()); const expandedDatasetRowContents = useMemo( () => - [...expandedDatasetIds].reduce>( - (aggregatedDatasetRows, datasetId) => { - return { - ...aggregatedDatasetRows, - [getFriendlyNameForPartitionId(datasetId)]: ( - - ), - }; - }, - {} - ), - [expandedDatasetIds, jobId, results, setTimeRange, timeRange] + [...expandedIds].reduce>((aggregatedDatasetRows, id) => { + const anomaly = results.anomalies.find((_anomaly) => _anomaly.id === id); + + return { + ...aggregatedDatasetRows, + [id]: anomaly ? ( + + ) : null, + }; + }, {}), + [expandedIds, results, timeRange, jobId] ); const [sorting, setSorting] = useState({ sort: { - field: 'topAnomalyScore', + field: 'anomalyScore', direction: 'desc', }, }); + const [_pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 20, + totalItemCount: results.anomalies.length, + pageSizeOptions: [10, 20, 50], + hidePerPageOptions: false, + }); + + const paginationOptions = useMemo(() => { + return { + ..._pagination, + totalItemCount: results.anomalies.length, + }; + }, [_pagination, results]); + const handleTableChange = useCallback( - ({ sort = {} }) => { + ({ page = {}, sort = {} }) => { + const { index, size } = page; + setPagination((currentPagination) => { + return { + ...currentPagination, + pageIndex: index, + pageSize: size, + }; + }); const { field, direction } = sort; setSorting({ sort: { @@ -107,33 +171,58 @@ export const AnomaliesTable: React.FunctionComponent<{ }, }); }, - [setSorting] + [setSorting, setPagination] ); const sortedTableItems = useMemo(() => { let sortedItems: TableItem[] = []; - if (sorting.sort.field === 'partitionName') { - sortedItems = tableItems.sort((a, b) => (a.partitionId > b.partitionId ? 1 : -1)); - } else if (sorting.sort.field === 'topAnomalyScore') { - sortedItems = tableItems.sort((a, b) => a.topAnomalyScore - b.topAnomalyScore); + if (sorting.sort.field === 'datasetName') { + sortedItems = tableItems.sort((a, b) => (a.datasetName > b.datasetName ? 1 : -1)); + } else if (sorting.sort.field === 'anomalyScore') { + sortedItems = tableItems.sort((a, b) => a.anomalyScore - b.anomalyScore); + } else if (sorting.sort.field === 'startTime') { + sortedItems = tableItems.sort((a, b) => a.startTime - b.startTime); } + return sorting.sort.direction === 'asc' ? sortedItems : sortedItems.reverse(); }, [tableItems, sorting]); + const pageOfItems: TableItem[] = useMemo(() => { + const { pageIndex, pageSize } = paginationOptions; + return sortedTableItems.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); + }, [paginationOptions, sortedTableItems]); + const columns: Array> = useMemo( () => [ { - field: 'partitionName', - name: partitionColumnName, + field: 'anomalyScore', + name: anomalyScoreColumnName, + sortable: true, + truncateText: true, + dataType: 'number' as const, + width: '130px', + render: (anomalyScore: number) => , + }, + { + field: 'anomalyMessage', + name: anomalyMessageColumnName, + sortable: false, + truncateText: true, + }, + { + field: 'startTime', + name: anomalyStartTimeColumnName, sortable: true, truncateText: true, + width: '230px', + render: (startTime: number) => moment(startTime).format(dateFormat), }, { - field: 'topAnomalyScore', - name: maxAnomalyScoreColumnName, + field: 'datasetName', + name: datasetColumnName, sortable: true, truncateText: true, - dataType: 'number' as const, + width: '200px', }, { align: RIGHT_ALIGNMENT, @@ -141,33 +230,28 @@ export const AnomaliesTable: React.FunctionComponent<{ isExpander: true, render: (item: TableItem) => ( ), }, ], - [collapseDataset, expandDataset, expandedDatasetIds] + [collapseId, expandId, expandedIds, dateFormat] ); return ( - ); }; - -const StyledEuiBasicTable: typeof EuiBasicTable = euiStyled(EuiBasicTable as any)` - & .euiTable { - table-layout: auto; - } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate_examples.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate_examples.ts new file mode 100644 index 000000000000..d3b30da72af9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate_examples.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { identity } from 'fp-ts/lib/function'; +import { npStart } from '../../../../legacy_singletons'; + +import { + getLogEntryRateExamplesRequestPayloadRT, + getLogEntryRateExamplesSuccessReponsePayloadRT, + LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH, +} from '../../../../../common/http_api/log_analysis'; +import { createPlainError, throwErrors } from '../../../../../common/runtime_types'; + +export const callGetLogEntryRateExamplesAPI = async ( + sourceId: string, + startTime: number, + endTime: number, + dataset: string, + exampleCount: number +) => { + const response = await npStart.http.fetch(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH, { + method: 'POST', + body: JSON.stringify( + getLogEntryRateExamplesRequestPayloadRT.encode({ + data: { + dataset, + exampleCount, + sourceId, + timeRange: { + startTime, + endTime, + }, + }, + }) + ), + }); + + return pipe( + getLogEntryRateExamplesSuccessReponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/setup_flyout.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/setup_flyout.tsx new file mode 100644 index 000000000000..0e9e34432f28 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/setup_flyout.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiText, + EuiSpacer, + EuiSteps, +} from '@elastic/eui'; + +import { + createInitialConfigurationStep, + createProcessStep, +} from '../../../components/logging/log_analysis_setup'; +import { useLogEntryRateSetup } from './use_log_entry_rate_setup'; + +interface LogEntryRateSetupFlyoutProps { + isOpen: boolean; + onClose: () => void; +} + +export const LogEntryRateSetupFlyout: React.FC = ({ + isOpen, + onClose, +}) => { + const { + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setValidatedIndices, + setUp, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResults, + } = useLogEntryRateSetup(); + + const viewResultsAndClose = useCallback(() => { + viewResults(); + onClose(); + }, [viewResults, onClose]); + + const steps = useMemo( + () => [ + createInitialConfigurationStep({ + setStartTime, + setEndTime, + startTime, + endTime, + isValidating, + validatedIndices, + setupStatus, + setValidatedIndices, + validationErrors, + }), + createProcessStep({ + cleanUpAndSetUp, + errorMessages: lastSetupErrorMessages, + isConfigurationValid: validationErrors.length <= 0 && !isValidating, + setUp, + setupStatus, + viewResults: viewResultsAndClose, + }), + ], + [ + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setUp, + setValidatedIndices, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResultsAndClose, + ] + ); + + if (!isOpen) { + return null; + } + return ( + + + +

+ +

+
+
+ + +

+ +

+
+ + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_examples.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_examples.ts new file mode 100644 index 000000000000..12bcdb2a4b4d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_examples.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo, useState } from 'react'; + +import { LogEntryRateExample } from '../../../../common/http_api'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callGetLogEntryRateExamplesAPI } from './service_calls/get_log_entry_rate_examples'; + +export const useLogEntryRateExamples = ({ + dataset, + endTime, + exampleCount, + sourceId, + startTime, +}: { + dataset: string; + endTime: number; + exampleCount: number; + sourceId: string; + startTime: number; +}) => { + const [logEntryRateExamples, setLogEntryRateExamples] = useState([]); + + const [getLogEntryRateExamplesRequest, getLogEntryRateExamples] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: async () => { + return await callGetLogEntryRateExamplesAPI( + sourceId, + startTime, + endTime, + dataset, + exampleCount + ); + }, + onResolve: ({ data: { examples } }) => { + setLogEntryRateExamples(examples); + }, + }, + [dataset, endTime, exampleCount, sourceId, startTime] + ); + + const isLoadingLogEntryRateExamples = useMemo( + () => getLogEntryRateExamplesRequest.state === 'pending', + [getLogEntryRateExamplesRequest.state] + ); + + const hasFailedLoadingLogEntryRateExamples = useMemo( + () => getLogEntryRateExamplesRequest.state === 'rejected', + [getLogEntryRateExamplesRequest.state] + ); + + return { + getLogEntryRateExamples, + hasFailedLoadingLogEntryRateExamples, + isLoadingLogEntryRateExamples, + logEntryRateExamples, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts index de2b873001cc..1cd27c64af53 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts @@ -10,6 +10,7 @@ import { GetLogEntryRateSuccessResponsePayload, LogEntryRateHistogramBucket, LogEntryRatePartition, + LogEntryRateAnomaly, } from '../../../../common/http_api/log_analysis'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { callGetLogEntryRateAPI } from './service_calls/get_log_entry_rate'; @@ -23,11 +24,16 @@ type PartitionRecord = Record< { buckets: PartitionBucket[]; topAnomalyScore: number; totalNumberOfLogEntries: number } >; +export type AnomalyRecord = LogEntryRateAnomaly & { + partitionId: string; +}; + export interface LogEntryRateResults { bucketDuration: number; totalNumberOfLogEntries: number; histogramBuckets: LogEntryRateHistogramBucket[]; partitionBuckets: PartitionRecord; + anomalies: AnomalyRecord[]; } export const useLogEntryRateResults = ({ @@ -55,6 +61,7 @@ export const useLogEntryRateResults = ({ totalNumberOfLogEntries: data.totalNumberOfLogEntries, histogramBuckets: data.histogramBuckets, partitionBuckets: formatLogEntryRateResultsByPartition(data), + anomalies: formatLogEntryRateResultsByAllAnomalies(data), }); }, onReject: () => { @@ -117,3 +124,23 @@ const formatLogEntryRateResultsByPartition = ( return resultsByPartition; }; + +const formatLogEntryRateResultsByAllAnomalies = ( + results: GetLogEntryRateSuccessResponsePayload['data'] +): AnomalyRecord[] => { + return results.histogramBuckets.reduce((anomalies, bucket) => { + return bucket.partitions.reduce((_anomalies, partition) => { + if (partition.anomalies.length > 0) { + partition.anomalies.forEach((anomaly) => { + _anomalies.push({ + partitionId: partition.partitionId, + ...anomaly, + }); + }); + return _anomalies; + } else { + return _anomalies; + } + }, anomalies); + }, []); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx index 83effaa3d51a..b1dc55fe5c18 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -62,7 +62,7 @@ export const IndicesConfigurationPanel = ({ id="xpack.infra.sourceConfiguration.logIndicesRecommendedValue" defaultMessage="The recommended value is {defaultValue}" values={{ - defaultValue: filebeat-*, + defaultValue: logs-*,filebeat-*, }} /> } 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 538cd5f7d952..3997a7eab44e 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 a5515895a51a..b56b409717cc 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 0b8773db2ddd..c2cde7eb15e9 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 2a218c1c78aa..3802366fe2ac 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/pages/metrics/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx index 7d4f35b19da7..b0aa67b5f081 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings.tsx @@ -15,7 +15,6 @@ export const MetricsSettingsPage = () => { ); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 1b28945320bb..2dda664a7f67 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 000000000000..3de4c40f47cc --- /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/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 46a0edf75b75..65ea53a8465b 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -4,90 +4,220 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraClientCoreSetup } from '../types'; -import { LogsFetchDataResponse } from '../../../observability/public'; +import { encode } from 'rison-node'; +import { i18n } from '@kbn/i18n'; +import { DEFAULT_SOURCE_ID } from '../../common/constants'; +import { InfraClientCoreSetup, InfraClientStartDeps } from '../types'; +import { + FetchData, + LogsFetchDataResponse, + HasData, + FetchDataParams, +} from '../../../observability/public'; +import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; +import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; -export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) { - return async () => { - // if you need the data plugin, this is how you get it - // const [, startPlugins] = await getStartServices(); - // const { data } = startPlugins; +interface StatsAggregation { + buckets: Array<{ key: string; doc_count: number }>; +} + +interface SeriesAggregation { + buckets: Array<{ + key_as_string: string; + key: number; + doc_count: number; + dataset: StatsAggregation; + }>; +} + +interface LogParams { + index: string; + timestampField: string; +} - // if you need a core dep, we need to pass in more than just getStartServices +type StatsAndSeries = Pick; - // perform query - return true; +export function getLogsHasDataFetcher( + getStartServices: InfraClientCoreSetup['getStartServices'] +): HasData { + return async () => { + const [core] = await getStartServices(); + const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch); + return sourceStatus.data.logIndicesExist; }; } export function getLogsOverviewDataFetcher( getStartServices: InfraClientCoreSetup['getStartServices'] -) { - return async (): Promise => { - // if you need the data plugin, this is how you get it - // const [, startPlugins] = await getStartServices(); - // const { data } = startPlugins; +): FetchData { + return async (params) => { + const [core, startPlugins] = await getStartServices(); + const { data } = startPlugins; + + const sourceConfiguration = await callFetchLogSourceConfigurationAPI( + DEFAULT_SOURCE_ID, + core.http.fetch + ); + + const { stats, series } = await fetchLogsOverview( + { + index: sourceConfiguration.data.configuration.logAlias, + timestampField: sourceConfiguration.data.configuration.fields.timestamp, + }, + params, + data + ); - // if you need a core dep, we need to pass in more than just getStartServices + const timeSpanInMinutes = + (Date.parse(params.endTime).valueOf() - Date.parse(params.startTime).valueOf()) / (1000 * 60); - // perform query return { - title: 'Log rate', - appLink: 'TBD', // TODO: what format should this be in, relative I assume? - stats: { - nginx: { - type: 'number', - label: 'nginx', - value: 345341, - }, - 'elasticsearch.audit': { - type: 'number', - label: 'elasticsearch.audit', - value: 164929, + title: i18n.translate('xpack.infra.logs.logOverview.logOverviewTitle', { + defaultMessage: 'Logs', + }), + appLink: `/app/logs/stream?logPosition=(end:${encode(params.endTime)},start:${encode( + params.startTime + )})`, + stats: normalizeStats(stats, timeSpanInMinutes), + series: normalizeSeries(series), + }; + }; +} + +async function fetchLogsOverview( + logParams: LogParams, + params: FetchDataParams, + dataPlugin: InfraClientStartDeps['data'] +): Promise { + const esSearcher = dataPlugin.search.getSearchStrategy('es'); + return new Promise((resolve, reject) => { + esSearcher + .search({ + params: { + index: logParams.index, + body: { + size: 0, + query: buildLogOverviewQuery(logParams, params), + aggs: buildLogOverviewAggregations(logParams, params), + }, }, - 'haproxy.log': { - type: 'number', - label: 'haproxy.log', - value: 51101, + }) + .subscribe( + (response) => { + if (response.rawResponse.aggregations) { + resolve(processLogsOverviewAggregations(response.rawResponse.aggregations)); + } else { + resolve({ stats: {}, series: {} }); + } }, + (error) => reject(error) + ); + }); +} + +function buildLogOverviewQuery(logParams: LogParams, params: FetchDataParams) { + return { + range: { + [logParams.timestampField]: { + gt: params.startTime, + lte: params.endTime, + format: 'strict_date_optional_time', }, - // Note: My understanding is that these series coordinates will be - // combined into objects that look like: - // { x: timestamp, y: value, g: label (e.g. nginx) } - // so they fit the stacked bar chart API - // https://elastic.github.io/elastic-charts/?path=/story/bar-chart--stacked-with-axis-and-legend - series: { - nginx: { - label: 'nginx', - coordinates: [ - { x: 1593000000000, y: 10014 }, - { x: 1593000900000, y: 12827 }, - { x: 1593001800000, y: 2946 }, - { x: 1593002700000, y: 14298 }, - { x: 1593003600000, y: 4096 }, - ], - }, - 'elasticsearch.audit': { - label: 'elasticsearch.audit', - coordinates: [ - { x: 1593000000000, y: 5676 }, - { x: 1593000900000, y: 6783 }, - { x: 1593001800000, y: 2394 }, - { x: 1593002700000, y: 4554 }, - { x: 1593003600000, y: 5659 }, - ], - }, - 'haproxy.log': { - label: 'haproxy.log', - coordinates: [ - { x: 1593000000000, y: 9085 }, - { x: 1593000900000, y: 9002 }, - { x: 1593001800000, y: 3940 }, - { x: 1593002700000, y: 5451 }, - { x: 1593003600000, y: 9133 }, - ], + }, + }; +} + +function buildLogOverviewAggregations(logParams: LogParams, params: FetchDataParams) { + return { + stats: { + terms: { + field: 'event.dataset', + size: 4, + }, + }, + series: { + date_histogram: { + field: logParams.timestampField, + fixed_interval: params.bucketSize, + }, + aggs: { + dataset: { + terms: { + field: 'event.dataset', + size: 4, + }, }, }, - }; + }, + }; +} + +function processLogsOverviewAggregations(aggregations: { + stats: StatsAggregation; + series: SeriesAggregation; +}): StatsAndSeries { + const processedStats = aggregations.stats.buckets.reduce( + (result, bucket) => { + result[bucket.key] = { + type: 'number', + label: bucket.key, + value: bucket.doc_count, + }; + + return result; + }, + {} + ); + + const processedSeries = aggregations.series.buckets.reduce( + (result, bucket) => { + const x = bucket.key; // the timestamp of the bucket + bucket.dataset.buckets.forEach((b) => { + const label = b.key; + result[label] = result[label] || { label, coordinates: [] }; + result[label].coordinates.push({ x, y: b.doc_count }); + }); + + return result; + }, + {} + ); + + return { + stats: processedStats, + series: processedSeries, }; } + +function normalizeStats( + stats: LogsFetchDataResponse['stats'], + timeSpanInMinutes: number +): LogsFetchDataResponse['stats'] { + return Object.keys(stats).reduce((normalized, key) => { + normalized[key] = { + ...stats[key], + value: stats[key].value / timeSpanInMinutes, + }; + return normalized; + }, {}); +} + +function normalizeSeries(series: LogsFetchDataResponse['series']): LogsFetchDataResponse['series'] { + const seriesKeys = Object.keys(series); + const timestamps = seriesKeys.flatMap((key) => series[key].coordinates.map((c) => c.x)); + const [first, second] = [...new Set(timestamps)].sort(); + const timeSpanInMinutes = (second - first) / (1000 * 60); + + return seriesKeys.reduce((normalized, key) => { + normalized[key] = { + ...series[key], + coordinates: series[key].coordinates.map((c) => { + if (c.y) { + return { ...c, y: c.y / timeSpanInMinutes }; + } + return c; + }), + }; + return normalized; + }, {}); +} diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts new file mode 100644 index 000000000000..6f9e41fbd08f --- /dev/null +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { coreMock } from 'src/core/public/mocks'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { CoreStart } from 'kibana/public'; +import { getLogsHasDataFetcher } from './logs_overview_fetchers'; +import { InfraClientStartDeps, InfraClientStartExports } from '../types'; +import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; + +// Note +// Calls to `.mock*` functions will fail the typecheck because how jest does the mocking. +// The calls will be preluded with a `@ts-expect-error` +jest.mock('../containers/logs/log_source/api/fetch_log_source_status'); + +function setup() { + const core = coreMock.createStart(); + const data = dataPluginMock.createStartContract(); + + const mockedGetStartServices = jest.fn(() => { + const deps = { data }; + return Promise.resolve([ + core as CoreStart, + deps as InfraClientStartDeps, + void 0 as InfraClientStartExports, + ]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>; + }); + return { core, mockedGetStartServices }; +} + +describe('Logs UI Observability Homepage Functions', () => { + describe('getLogsHasDataFetcher()', () => { + beforeEach(() => { + // @ts-expect-error + callFetchLogSourceStatusAPI.mockReset(); + }); + it('should return true when some index is present', async () => { + const { mockedGetStartServices } = setup(); + + // @ts-expect-error + callFetchLogSourceStatusAPI.mockResolvedValue({ + data: { logIndexFields: [], logIndicesExist: true }, + }); + + const hasData = getLogsHasDataFetcher(mockedGetStartServices); + const response = await hasData(); + + expect(callFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(response).toBe(true); + }); + + it('should return false when no index is present', async () => { + const { mockedGetStartServices } = setup(); + + // @ts-expect-error + callFetchLogSourceStatusAPI.mockResolvedValue({ + data: { logIndexFields: [], logIndicesExist: false }, + }); + + const hasData = getLogsHasDataFetcher(mockedGetStartServices); + const response = await hasData(); + + expect(callFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(response).toBe(false); + }); + }); + + describe('getLogsOverviewDataFetcher()', () => { + it.skip('should work', async () => { + // Pending + }); + }); +}); diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 6fbdeff950d1..8af37a36ef74 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -15,6 +15,7 @@ import { initGetLogEntryCategoryDatasetsRoute, initGetLogEntryCategoryExamplesRoute, initGetLogEntryRateRoute, + initGetLogEntryRateExamplesRoute, initValidateLogAnalysisDatasetsRoute, initValidateLogAnalysisIndicesRoute, } from './routes/log_analysis'; @@ -56,6 +57,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initValidateLogAnalysisDatasetsRoute(libs); initValidateLogAnalysisIndicesRoute(libs); initLogEntriesRoute(libs); + initGetLogEntryRateExamplesRoute(libs); initLogEntriesHighlightsRoute(libs); initLogEntriesSummaryRoute(libs); initLogEntriesSummaryHighlightsRoute(libs); 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 f4dc7de51dae..e3d23d86c9f5 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 445911878111..1ef86d9e7eac 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 ba43303bccf0..b865454951cd 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 5782277e4f46..4c02593dd009 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 39db24684e8d..0ecfa27d0f0a 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/log_analysis/log_entry_rate_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts index 125cc2b196e0..290cf03b6736 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts @@ -7,16 +7,30 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { getJobId } from '../../../common/log_analysis'; +import { RequestHandlerContext } from 'src/core/server'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { NoLogAnalysisResultsIndexError } from './errors'; import { logRateModelPlotResponseRT, createLogEntryRateQuery, LogRateModelPlotBucket, CompositeTimestampPartitionKey, } from './queries'; -import { MlSystem } from '../../types'; +import { startTracingSpan } from '../../../common/performance_tracing'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { getJobId, jobCustomSettingsRT } from '../../../common/log_analysis'; +import { + createLogEntryRateExamplesQuery, + logEntryRateExamplesResponseRT, +} from './queries/log_entry_rate_examples'; +import { + InsufficientLogAnalysisMlJobConfigurationError, + NoLogAnalysisMlJobError, + NoLogAnalysisResultsIndexError, +} from './errors'; +import { InfraSource } from '../sources'; +import type { MlSystem } from '../../types'; +import { InfraRequestHandlerContext } from '../../types'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; @@ -73,6 +87,7 @@ export async function getLogEntryRateBuckets( partitions: Array<{ analysisBucketCount: number; anomalies: Array<{ + id: string; actualLogEntryRate: number; anomalyScore: number; duration: number; @@ -91,7 +106,8 @@ export async function getLogEntryRateBuckets( const partition = { analysisBucketCount: timestampPartitionBucket.filter_model_plot.doc_count, anomalies: timestampPartitionBucket.filter_records.top_hits_record.hits.hits.map( - ({ _source: record }) => ({ + ({ _id, _source: record }) => ({ + id: _id, actualLogEntryRate: record.actual[0], anomalyScore: record.record_score, duration: record.bucket_span * 1000, @@ -127,3 +143,130 @@ export async function getLogEntryRateBuckets( } }, []); } + +export async function getLogEntryRateExamples( + context: RequestHandlerContext & { infra: Required }, + sourceId: string, + startTime: number, + endTime: number, + dataset: string, + exampleCount: number, + sourceConfiguration: InfraSource, + callWithRequest: KibanaFramework['callWithRequest'] +) { + const finalizeLogEntryRateExamplesSpan = startTracingSpan( + 'get log entry rate example log entries' + ); + + const jobId = getJobId(context.infra.spaceId, sourceId, 'log-entry-rate'); + + const { + mlJob, + timing: { spans: fetchMlJobSpans }, + } = await fetchMlJob(context, jobId); + + const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings); + const indices = customSettings?.logs_source_config?.indexPattern; + const timestampField = customSettings?.logs_source_config?.timestampField; + const tiebreakerField = sourceConfiguration.configuration.fields.tiebreaker; + + if (indices == null || timestampField == null) { + throw new InsufficientLogAnalysisMlJobConfigurationError( + `Failed to find index configuration for ml job ${jobId}` + ); + } + + const { + examples, + timing: { spans: fetchLogEntryRateExamplesSpans }, + } = await fetchLogEntryRateExamples( + context, + indices, + timestampField, + tiebreakerField, + startTime, + endTime, + dataset, + exampleCount, + callWithRequest + ); + + const logEntryRateExamplesSpan = finalizeLogEntryRateExamplesSpan(); + + return { + data: examples, + timing: { + spans: [logEntryRateExamplesSpan, ...fetchMlJobSpans, ...fetchLogEntryRateExamplesSpans], + }, + }; +} + +export async function fetchLogEntryRateExamples( + context: RequestHandlerContext & { infra: Required }, + indices: string, + timestampField: string, + tiebreakerField: string, + startTime: number, + endTime: number, + dataset: string, + exampleCount: number, + callWithRequest: KibanaFramework['callWithRequest'] +) { + const finalizeEsSearchSpan = startTracingSpan('Fetch log rate examples from ES'); + + const { + hits: { hits }, + } = decodeOrThrow(logEntryRateExamplesResponseRT)( + await callWithRequest( + context, + 'search', + createLogEntryRateExamplesQuery( + indices, + timestampField, + tiebreakerField, + startTime, + endTime, + dataset, + exampleCount + ) + ) + ); + + const esSearchSpan = finalizeEsSearchSpan(); + + return { + examples: hits.map((hit) => ({ + id: hit._id, + dataset, + message: hit._source.message ?? '', + timestamp: hit.sort[0], + tiebreaker: hit.sort[1], + })), + timing: { + spans: [esSearchSpan], + }, + }; +} + +async function fetchMlJob( + context: RequestHandlerContext & { infra: Required }, + logEntryRateJobId: string +) { + const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); + const { + jobs: [mlJob], + } = await context.infra.mlAnomalyDetectors.jobs(logEntryRateJobId); + + const mlGetJobSpan = finalizeMlGetJobSpan(); + + if (mlJob == null) { + throw new NoLogAnalysisMlJobError(`Failed to find ml job ${logEntryRateJobId}.`); + } + + return { + mlJob, + timing: { + spans: [mlGetJobSpan], + }, + }; +} diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts index 269850e29263..8d9c586b2ef6 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts @@ -143,6 +143,7 @@ export const logRateModelPlotBucketRT = rt.type({ hits: rt.type({ hits: rt.array( rt.type({ + _id: rt.string, _source: logRateMlRecordRT, }) ), diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate_examples.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate_examples.ts new file mode 100644 index 000000000000..ef06641caf79 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate_examples.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; +import { defaultRequestParameters } from './common'; +import { partitionField } from '../../../../common/log_analysis'; + +export const createLogEntryRateExamplesQuery = ( + indices: string, + timestampField: string, + tiebreakerField: string, + startTime: number, + endTime: number, + dataset: string, + exampleCount: number +) => ({ + ...defaultRequestParameters, + body: { + query: { + bool: { + filter: [ + { + range: { + [timestampField]: { + gte: startTime, + lte: endTime, + }, + }, + }, + { + term: { + [partitionField]: dataset, + }, + }, + ], + }, + }, + sort: [{ [timestampField]: 'asc' }, { [tiebreakerField]: 'asc' }], + }, + _source: ['event.dataset', 'message'], + index: indices, + size: exampleCount, +}); + +export const logEntryRateExampleHitRT = rt.type({ + _id: rt.string, + _source: rt.partial({ + event: rt.partial({ + dataset: rt.string, + }), + message: rt.string, + }), + sort: rt.tuple([rt.number, rt.number]), +}); + +export type LogEntryRateExampleHit = rt.TypeOf; + +export const logEntryRateExamplesResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + hits: rt.array(logEntryRateExampleHitRT), + }), + }), +]); + +export type LogEntryRateExamplesResponse = rt.TypeOf; 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 bb1c4c6632af..317a7da95ce6 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/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index ba22b4db62d6..b096bed84fa9 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -9,8 +9,8 @@ import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; export const defaultSourceConfiguration: InfraSourceConfiguration = { name: 'Default', description: '', - metricAlias: 'metricbeat-*', - logAlias: 'filebeat-*,kibana_sample_data_logs*', + metricAlias: 'metrics-*,metricbeat-*', + logAlias: 'logs-*,filebeat-*,kibana_sample_data_logs*', fields: { container: 'container.id', host: 'host.name', diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts new file mode 100644 index 000000000000..59a22d33de85 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { migrationMocks } from 'src/core/server/mocks'; +import { addNewIndexingStrategyIndexNames } from './7_9_0_add_new_indexing_strategy_index_names'; +import { infraSourceConfigurationSavedObjectName } from '../saved_object_type'; + +describe('infra source configuration migration function for 7.9.0', () => { + test('adds "logs-*" when the logAlias contains "filebeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'filebeat-*,custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual( + createTestSourceConfiguration('filebeat-*,custom-log-index-*,logs-*', 'custom-metric-index-*') + ); + }); + + test('doesn\'t add "logs-*" when the logAlias doesn\'t contain "filebeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); + + test('doesn\'t add "logs-*" when the logAlias already contains it', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'filebeat-*,logs-*,custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); + + test('adds "metrics-*" when the logAlias contains "metricbeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'metricbeat-*,custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual( + createTestSourceConfiguration( + 'custom-log-index-*', + 'metricbeat-*,custom-metric-index-*,metrics-*' + ) + ); + }); + + test('doesn\'t add "metrics-*" when the logAlias doesn\'t contain "metricbeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); + + test('doesn\'t add "metrics-*" when the metricAlias already contains it', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'metrics-*,metricbeat-*,custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); +}); + +const createTestSourceConfiguration = (logAlias: string, metricAlias: string) => ({ + attributes: { + name: 'TEST CONFIGURATION', + description: '', + fields: { + pod: 'TEST POD FIELD', + host: 'TEST HOST FIELD', + message: ['TEST MESSAGE FIELD'], + container: 'TEST CONTAINER FIELD', + timestamp: 'TEST TIMESTAMP FIELD', + tiebreaker: 'TEST TIEBREAKER FIELD', + }, + inventoryDefaultView: '0', + metricsExplorerDefaultView: '0', + logColumns: [ + { + fieldColumn: { + id: 'TEST FIELD COLUMN ID', + field: 'TEST FIELD COLUMN FIELD', + }, + }, + ], + logAlias, + metricAlias, + }, + id: 'TEST_ID', + type: infraSourceConfigurationSavedObjectName, +}); diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts new file mode 100644 index 000000000000..0d5563191d1b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectMigrationFn } from 'src/core/server'; +import { InfraSourceConfiguration } from '../../../../common/http_api/source_api'; + +export const addNewIndexingStrategyIndexNames: SavedObjectMigrationFn< + InfraSourceConfiguration, + InfraSourceConfiguration +> = (sourceConfigurationDocument) => { + const oldLogAliasSegments = sourceConfigurationDocument.attributes.logAlias.split(','); + const oldMetricAliasSegments = sourceConfigurationDocument.attributes.metricAlias.split(','); + + const newLogAliasSegment = 'logs-*'; + const newMetricAliasSegment = 'metrics-*'; + + return { + ...sourceConfigurationDocument, + attributes: { + ...sourceConfigurationDocument.attributes, + logAlias: + oldLogAliasSegments.includes('filebeat-*') && + !oldLogAliasSegments.includes(newLogAliasSegment) + ? [...oldLogAliasSegments, newLogAliasSegment].join(',') + : sourceConfigurationDocument.attributes.logAlias, + metricAlias: + oldMetricAliasSegments.includes('metricbeat-*') && + !oldMetricAliasSegments.includes(newMetricAliasSegment) + ? [...oldMetricAliasSegments, newMetricAliasSegment].join(',') + : sourceConfigurationDocument.attributes.metricAlias, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts index a36ef8d1a892..11db18d6bf79 100644 --- a/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsType } from 'src/core/server'; +import { addNewIndexingStrategyIndexNames } from './migrations/7_9_0_add_new_indexing_strategy_index_names'; export const infraSourceConfigurationSavedObjectName = 'infrastructure-ui-source'; @@ -86,4 +86,7 @@ export const infraSourceConfigurationSavedObjectType: SavedObjectsType = { }, }, }, + migrations: { + '7.9.0': addNewIndexingStrategyIndexNames, + }, }; 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 28b7777c1d68..08ad266a22f9 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/log_analysis/results/index.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts index 15615046bdd6..30b6be435837 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts @@ -8,3 +8,4 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; export * from './log_entry_category_examples'; export * from './log_entry_rate'; +export * from './log_entry_rate_examples'; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts new file mode 100644 index 000000000000..b8ebcc66911d --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { createValidationFunction } from '../../../../common/runtime_types'; +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { NoLogAnalysisResultsIndexError, getLogEntryRateExamples } from '../../../lib/log_analysis'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; +import { + getLogEntryRateExamplesRequestPayloadRT, + getLogEntryRateExamplesSuccessReponsePayloadRT, + LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH, +} from '../../../../common/http_api/log_analysis'; + +export const initGetLogEntryRateExamplesRoute = ({ framework, sources }: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH, + validate: { + body: createValidationFunction(getLogEntryRateExamplesRequestPayloadRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { + data: { + dataset, + exampleCount, + sourceId, + timeRange: { startTime, endTime }, + }, + } = request.body; + + const sourceConfiguration = await sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + + try { + assertHasInfraMlPlugins(requestContext); + + const { data: logEntryRateExamples, timing } = await getLogEntryRateExamples( + requestContext, + sourceId, + startTime, + endTime, + dataset, + exampleCount, + sourceConfiguration, + framework.callWithRequest + ); + + return response.ok({ + body: getLogEntryRateExamplesSuccessReponsePayloadRT.encode({ + data: { + examples: logEntryRateExamples, + }, + timing, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + if (error instanceof NoLogAnalysisResultsIndexError) { + return response.notFound({ body: { message: error.message } }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index 7e3a30e1e691..b2664d5a6d9f 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 559fba079998..2b65c4241072 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 97b5cca36929..3d3c91a4310f 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/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index 9617173bd0c7..c374cbb3bb14 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -3520,7 +3520,17 @@ ] } }, - "/fleet/agents/unenroll": { + "/fleet/agents/{agentId}/unenroll": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], "post": { "summary": "Fleet - Agent - Unenroll", "tags": [], @@ -3530,7 +3540,26 @@ { "$ref": "#/components/parameters/xsrfHeader" } - ] + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "force": { "type": "boolean" } + } + }, + "examples": { + "example-1": { + "value": { + "force": true + } + } + } + } + } + } } }, "/fleet/config/{configId}/agent-status": { @@ -4096,6 +4125,12 @@ "enrolled_at": { "type": "string" }, + "unenrolled_at": { + "type": "string" + }, + "unenrollment_started_at": { + "type": "string" + }, "shared_id": { "type": "string" }, diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index cc1c2da71051..b1d92d3a78e6 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -21,6 +21,9 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta if (!agent.active) { return 'inactive'; } + if (agent.unenrollment_started_at && !agent.unenrolled_at) { + return 'unenrolling'; + } if (agent.current_error_events.length > 0) { return 'error'; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index d2a2a3f5705a..27f0c61685fd 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -11,10 +11,10 @@ export type AgentType = | typeof AGENT_TYPE_PERMANENT | typeof AGENT_TYPE_TEMPORARY; -export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning'; - +export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning' | 'unenrolling'; +export type AgentActionType = 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE' | 'UNENROLL'; export interface NewAgentAction { - type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE'; + type: AgentActionType; data?: any; sent_at?: string; } @@ -26,7 +26,7 @@ export interface AgentAction extends NewAgentAction { } export interface AgentActionSOAttributes { - type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE'; + type: AgentActionType; sent_at?: string; timestamp?: string; created_at: string; @@ -73,6 +73,8 @@ interface AgentBase { type: AgentType; active: boolean; enrolled_at: string; + unenrolled_at?: string; + unenrollment_started_at?: string; shared_id?: string; access_api_key_id?: string; default_api_key?: string; 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 5b68cd2beeed..3ee3039e9e1c 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -32,10 +32,10 @@ export enum KibanaAssetType { } export enum ElasticsearchAssetType { - componentTemplate = 'component-template', - ingestPipeline = 'ingest-pipeline', - indexTemplate = 'index-template', - ilmPolicy = 'ilm-policy', + componentTemplate = 'component_template', + ingestPipeline = 'ingest_pipeline', + indexTemplate = 'index_template', + ilmPolicy = 'ilm_policy', } export enum AgentAssetType { @@ -243,17 +243,13 @@ export type AssetReference = Pick & { * Types of assets which can be installed/removed */ export enum IngestAssetType { - DataFrameTransform = 'data-frame-transform', - IlmPolicy = 'ilm-policy', - IndexTemplate = 'index-template', - ComponentTemplate = 'component-template', - IngestPipeline = 'ingest-pipeline', - MlJob = 'ml-job', - RollupJob = 'rollup-job', + IlmPolicy = 'ilm_policy', + IndexTemplate = 'index_template', + ComponentTemplate = 'component_template', + IngestPipeline = 'ingest_pipeline', } 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 c52471ccfb4f..0d1f72afa16f 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 6ebfd3f28fd9..36b7d412bf27 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 000000000000..b00809249897 --- /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 70668c2856f9..849d7bfc63f3 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 19243084f682..42d1075e2ee1 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 0a9daf0038aa..4e79bd4fa799 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/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx index 54cb5171f5a3..31c6d7644644 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx @@ -17,11 +17,11 @@ export const DisplayedAssets: ServiceNameToAssetTypes = { export const AssetTitleMap: Record = { dashboard: 'Dashboard', - 'ilm-policy': 'ILM Policy', - 'ingest-pipeline': 'Ingest Pipeline', + ilm_policy: 'ILM Policy', + ingest_pipeline: 'Ingest Pipeline', 'index-pattern': 'Index Pattern', - 'index-template': 'Index Template', - 'component-template': 'Component Template', + index_template: 'Index Template', + component_template: 'Component Template', search: 'Saved Search', visualization: 'Visualization', input: 'Agent input', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 75d055675514..6d04f63702c6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -236,7 +236,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, { field: 'active', - width: '100px', + width: '120px', name: i18n.translate('xpack.ingestManager.agentList.statusColumnTitle', { defaultMessage: 'Status', }), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx index 181ebe350422..e4dfa520259e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx @@ -53,6 +53,14 @@ const Status = { /> ), + Unenrolling: ( + + + + ), }; function getStatusComponent(agent: Agent): React.ReactElement { @@ -65,6 +73,8 @@ function getStatusComponent(agent: Agent): React.ReactElement { return Status.Offline; case 'warning': return Status.Warning; + case 'unenrolling': + return Status.Unenrolling; default: return Status.Online; } 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 450def54ba1d..592ca7f7b838 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/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx index fec2253c0dd5..90d8ff545341 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx @@ -74,7 +74,7 @@ export const AgentUnenrollProvider: React.FunctionComponent = ({ children const successMessage = i18n.translate( 'xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { - defaultMessage: "Unenrolled agent '{id}'", + defaultMessage: "Unenrolling agent '{id}'", values: { id: agentId }, } ); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index d31498599a2b..d9a957223712 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -13,7 +13,6 @@ import { GetOneAgentEventsResponse, PostAgentCheckinResponse, PostAgentEnrollResponse, - PostAgentUnenrollResponse, GetAgentStatusResponse, PutAgentReassignResponse, } from '../../../common/types'; @@ -25,7 +24,6 @@ import { GetOneAgentEventsRequestSchema, PostAgentCheckinRequestSchema, PostAgentEnrollRequestSchema, - PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PutAgentReassignRequestSchema, } from '../../types'; @@ -302,25 +300,6 @@ export const getAgentsHandler: RequestHandler< } }; -export const postAgentsUnenrollHandler: RequestHandler> = async (context, request, response) => { - const soClient = context.core.savedObjects.client; - try { - await AgentService.unenrollAgent(soClient, request.params.agentId); - - const body: PostAgentUnenrollResponse = { - success: true, - }; - return response.ok({ body }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); - } -}; - export const putAgentsReassignHandler: RequestHandler< TypeOf, undefined, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index eaab46c7b455..d7eec50eac3c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -33,7 +33,6 @@ import { getAgentEventsHandler, postAgentCheckinHandler, postAgentEnrollHandler, - postAgentsUnenrollHandler, getAgentStatusForConfigHandler, putAgentsReassignHandler, } from './handlers'; @@ -41,6 +40,7 @@ import { postAgentAcksHandlerBuilder } from './acks_handlers'; import * as AgentService from '../../services/agents'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; import { appContextService } from '../../services'; +import { postAgentsUnenrollHandler } from './unenroll_handler'; export const registerRoutes = (router: IRouter) => { // Get one diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts new file mode 100644 index 000000000000..d1e54fe4fb3a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandler } from 'src/core/server'; +import { TypeOf } from '@kbn/config-schema'; +import { PostAgentUnenrollResponse } from '../../../common/types'; +import { PostAgentUnenrollRequestSchema } from '../../types'; +import * as AgentService from '../../services/agents'; + +export const postAgentsUnenrollHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + if (request.body?.force === true) { + await AgentService.forceUnenrollAgent(soClient, request.params.agentId); + } else { + await AgentService.unenrollAgent(soClient, request.params.agentId); + } + + const body: PostAgentUnenrollResponse = { + success: true, + }; + return response.ok({ body }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; 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 98de9ac217af..b47cf4f7e7c3 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -54,6 +54,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { type: { type: 'keyword' }, active: { type: 'boolean' }, enrolled_at: { type: 'date' }, + unenrolled_at: { type: 'date' }, + unenrollment_started_at: { type: 'date' }, access_api_key_id: { type: 'keyword' }, version: { type: 'keyword' }, user_provided_metadata: { type: 'flattened' }, @@ -119,8 +121,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' }, @@ -314,6 +315,9 @@ export function registerEncryptedSavedObjects( 'config_newest_revision', 'updated_at', 'current_error_events', + 'unenrolled_at', + 'unenrollment_started_at', + 'packages', ]), }); encryptedSavedObjects.registerType({ 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 ada35d182506..bd00727714c3 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/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index c59bac6a5469..1dfe4e067daf 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -26,6 +26,7 @@ import { AGENT_ACTION_SAVED_OBJECT_TYPE, } from '../../constants'; import { getAgentActionByIds } from './actions'; +import { forceUnenrollAgent } from './unenroll'; const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; @@ -63,6 +64,12 @@ export async function acknowledgeAgentActions( if (actions.length === 0) { return []; } + + const isAgentUnenrolled = actions.some((action) => action.type === 'UNENROLL'); + if (isAgentUnenrolled) { + await forceUnenrollAgent(soClient, agent.id); + } + const config = getLatestConfigIfUpdated(agent, actions); await soClient.bulkUpdate([ 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 c78a9ff8bb7b..4420135aec95 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 b6d87c9ca5b2..55970607c74a 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 0efb202eff53..016a2344cf53 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/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index ee7e08d74103..e0ac2620cafd 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -9,8 +9,21 @@ import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; import * as APIKeyService from '../api_keys'; +import { createAgentAction } from './actions'; export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { + const now = new Date().toISOString(); + await createAgentAction(soClient, { + agent_id: agentId, + created_at: now, + type: 'UNENROLL', + }); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + unenrollment_started_at: now, + }); +} + +export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = await getAgent(soClient, agentId); await Promise.all([ @@ -21,7 +34,9 @@ export async function unenrollAgent(soClient: SavedObjectsClientContract, agentI ? APIKeyService.invalidateAPIKey(soClient, agent.default_api_key_id) : undefined, ]); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { active: false, + unenrolled_at: new Date().toISOString(), }); } 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 3b003f47eb6f..02e2c8151fac 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 635742c82f9a..f5fec020bf5b 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 06c07da6cd77..2de378f71753 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 c886f4868ad3..5a7546bfee2e 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/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 5526e889124f..a508c33e0347 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -70,6 +70,11 @@ export const PostAgentUnenrollRequestSchema = { params: schema.object({ agentId: schema.string(), }), + body: schema.nullable( + schema.object({ + force: schema.boolean(), + }) + ), }; export const PutAgentReassignRequestSchema = { 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 2c8134d2e8f9..dc0f11168049 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 251a2ffe9521..d22365344281 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 a68e667f4ab4..341e15132d35 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 52d1a77c1df6..0e7a45e8d07b 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 6d1e2610b5c2..5e5cddbd36b9 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 5435f43a78ac..000000000000 --- 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 bd74f09546ff..aa52c14e61ea 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 000000000000..d29af67d3179 --- /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 7ad9aed3c44a..cc3817d92d5e 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 15121cc71c32..a4bbf840dff7 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 2d512a6bfa2e..de0621b18723 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 000000000000..482878d1bda5 --- /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 000000000000..c1c49f251d51 --- /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 000000000000..2f4cdce1edd0 --- /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 000000000000..f183386d5927 --- /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 000000000000..c89ff1d3d99a --- /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 5bbea4b89b05..5cee5311c62a 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 913902d29550..ab080767b602 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 0fe804adaeb4..09c047d1d51b 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 9d284748a3d1..3eccda55fbb3 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 46e3d1c803fd..87e6eb7f642a 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 fbc06f41208f..ec864d31d198 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 000000000000..9b7c2069fcdd --- /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 fa1d041bdaba..1e9a97e189a5 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 000000000000..7c62383024cf --- /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 000000000000..6c544b31df43 --- /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 000000000000..108b22be43ca --- /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 58d6e492b85e..89bc50fc0600 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 7257677c08fc..000000000000 --- 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 09e77c510775..000000000000 --- 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 7265f63f45a5..0e06b8d55d37 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 4e069aab8bdd..295e7ff14111 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 aa39fca29fa8..aea8f0f0910f 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 05e7d1e41c5f..d2c4b73a4876 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/kibana.json b/x-pack/plugins/lens/kibana.json index 346a5a24c269..7da5eaed5155 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -10,7 +10,8 @@ "navigation", "kibanaLegacy", "visualizations", - "dashboard" + "dashboard", + "charts" ], "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"], "configPath": ["xpack", "lens"], 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 d62f3dbcf029..b41e93def966 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/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 60c31e5d090e..f21939b3a289 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -68,29 +68,33 @@ export function WorkspacePanelWrapper({ return ( - + + + + + {activeVisualization && activeVisualization.renderToolbar && ( + + + + )} + - {activeVisualization && activeVisualization.renderToolbar && ( - - - - )} {(!emptyExpression || title) && ( 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 851a9f4653fe..94c0f4083dfe 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.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index f70df855fe0c..0d60bd588f71 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -17,6 +17,7 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; const initialState: IndexPatternPrivateState = { indexPatternRefs: [], @@ -230,6 +231,7 @@ describe('IndexPattern Data Panel', () => { fromDate: 'now-7d', toDate: 'now', }, + charts: chartPluginMock.createSetupContract(), query: { query: '', language: 'lucene' }, filters: [], showNoDataPopover: jest.fn(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 87fbf81fceba..eb7940634d78 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, @@ -47,9 +47,11 @@ export type Props = DatasourceDataPanelProps & { state: IndexPatternPrivateState, setState: StateSetter ) => void; + charts: ChartsPluginSetup; }; import { LensFieldIcon } from './lens_field_icon'; import { ChangeIndexPattern } from './change_indexpattern'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; // TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted const FixedEuiContextMenuPanel = (EuiContextMenuPanel as unknown) as React.FunctionComponent< @@ -82,6 +84,7 @@ export function IndexPatternDataPanel({ filters, dateRange, changeIndexPattern, + charts, showNoDataPopover, }: Props) { const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; @@ -170,6 +173,7 @@ export function IndexPatternDataPanel({ dragDropContext={dragDropContext} core={core} data={data} + charts={charts} onChangeIndexPattern={onChangeIndexPattern} existingFields={state.existingFields} /> @@ -214,6 +218,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ core, data, existingFields, + charts, }: Omit & { data: DataPublicPluginStart; currentIndexPatternId: string; @@ -222,6 +227,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ dragDropContext: DragContextState; onChangeIndexPattern: (newId: string) => void; existingFields: IndexPatternPrivateState['existingFields']; + charts: ChartsPluginSetup; }) { const [localState, setLocalState] = useState({ nameFilter: '', @@ -250,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 ( @@ -376,6 +382,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ dateRange, query, filters, + chartsThemeService: charts.theme, }), [core, data, currentIndexPattern, dateRange, query, filters, localState.nameFilter] ); 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 df49ed828a19..ebb706258cf4 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 34a4384ec0d4..5b84108b99dd 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/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index e8dfbc250c53..0a3af97f8ad7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -13,6 +13,9 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { IndexPattern } from './types'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; + +const chartsThemeService = chartPluginMock.createSetupContract().theme; describe('IndexPattern Field Item', () => { let defaultProps: FieldItemProps; @@ -80,6 +83,7 @@ describe('IndexPattern Field Item', () => { searchable: true, }, exists: true, + chartsThemeService, }; data.fieldFormats = ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 1a1a34d30f8a..815725f4331a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -20,7 +20,6 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { Axis, BarSeries, @@ -41,6 +40,7 @@ import { esQuery, IIndexPattern, } from '../../../../../src/plugins/data/public'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { DraggedField } from './indexpattern'; import { DragDrop } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; @@ -60,6 +60,7 @@ export interface FieldItemProps { exists: boolean; query: Query; dateRange: DatasourceDataPanelProps['dateRange']; + chartsThemeService: ChartsPluginSetup['theme']; filters: Filter[]; hideDetails?: boolean; } @@ -254,11 +255,12 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { dateRange, core, sampledValues, + chartsThemeService, data: { fieldFormats }, } = props; - const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); - const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + const chartTheme = chartsThemeService.useChartsTheme(); + const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); let histogramDefault = !!props.histogram; const totalValuesCount = @@ -410,6 +412,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { - + { let defaultProps: FieldsAccordionProps; @@ -56,6 +57,7 @@ describe('Fields Accordion', () => { }, query: { query: '', language: 'lucene' }, filters: [], + chartsThemeService: chartPluginMock.createSetupContract().theme, }; defaultProps = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index b756cf81a907..7cc049c107b8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -19,10 +19,12 @@ import { FieldItem } from './field_item'; import { Query, Filter } from '../../../../../src/plugins/data/public'; import { DatasourceDataPanelProps } from '../types'; import { IndexPattern } from './types'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; export interface FieldItemSharedProps { core: DatasourceDataPanelProps['core']; data: DataPublicPluginStart; + chartsThemeService: ChartsPluginSetup['theme']; indexPattern: IndexPattern; highlight?: string; query: Query; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 73fd144b9c7f..45d0ee45fab4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -9,6 +9,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -19,6 +20,7 @@ export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; data: DataPublicPluginSetup; editorFrame: EditorFrameSetup; + charts: ChartsPluginSetup; } export interface IndexPatternDatasourceStartPlugins { @@ -30,7 +32,7 @@ export class IndexPatternDatasource { setup( core: CoreSetup, - { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + { expressions, editorFrame, charts }: IndexPatternDatasourceSetupPlugins ) { expressions.registerFunction(renameColumns); @@ -40,6 +42,7 @@ export class IndexPatternDatasource { core: coreStart, storage: new Storage(localStorage), data, + charts, }) ) as Promise ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 6a79ce450cd9..3bd0685551a4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -11,6 +11,7 @@ import { coreMock } from 'src/core/public/mocks'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { Ast } from '@kbn/interpreter/common'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -140,6 +141,7 @@ describe('IndexPattern Data Source', () => { storage: {} as IStorageWrapper, core: coreMock.createStart(), data: dataPluginMock.createStartContract(), + charts: chartPluginMock.createSetupContract(), }); persistedState = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index a98f63cf9b36..e9d095bfbcef 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -46,6 +46,7 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '../index'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; export { OperationType, IndexPatternColumn } from './operations'; @@ -102,10 +103,12 @@ export function getIndexPatternDatasource({ core, storage, data, + charts, }: { core: CoreStart; storage: IStorageWrapper; data: DataPublicPluginStart; + charts: ChartsPluginSetup; }) { const savedObjectsClient = core.savedObjects.client; const uiSettings = core.uiSettings; @@ -212,6 +215,7 @@ export function getIndexPatternDatasource({ }); }} data={data} + charts={charts} {...props} /> , 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 89e2c753f4c7..111a113a16be 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 eea00d52a77f..1ae10e07b0c2 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 101f53699336..e995c7317b5d 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 a04f71a9095c..9e5a0f496357 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 3a1aaaa819dc..51691ae18a99 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 e507bee2a898..9473a1523b8c 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 fadee01e695d..0cd92fd96c95 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/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts index dd828c6c3530..401b6d634c69 100644 --- a/x-pack/plugins/lens/public/pie_visualization/index.ts +++ b/x-pack/plugins/lens/public/pie_visualization/index.ts @@ -4,18 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { CoreSetup } from 'src/core/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { pieVisualization } from './pie_visualization'; import { pie, getPieRenderer } from './register_expression'; import { EditorFrameSetup, FormatFactory } from '../types'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; export interface PieVisualizationPluginSetupPlugins { editorFrame: EditorFrameSetup; expressions: ExpressionsSetup; formatFactory: Promise; + charts: ChartsPluginSetup; } export interface PieVisualizationPluginStartPlugins { @@ -27,17 +28,14 @@ export class PieVisualization { setup( core: CoreSetup, - { expressions, formatFactory, editorFrame }: PieVisualizationPluginSetupPlugins + { expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => pie); expressions.registerRenderer( getPieRenderer({ formatFactory, - chartTheme: core.uiSettings.get('theme:darkMode') - ? EUI_CHARTS_THEME_DARK.theme - : EUI_CHARTS_THEME_LIGHT.theme, - isDarkMode: core.uiSettings.get('theme:darkMode'), + chartsThemeService: charts.theme, }) ); diff --git a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx index bbc6a1dc75c3..cea84db8b279 100644 --- a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx @@ -8,7 +8,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; -import { PartialTheme } from '@elastic/charts'; import { IInterpreterRenderHandlers, ExpressionRenderDefinition, @@ -17,6 +16,7 @@ import { import { LensMultiTable, FormatFactory, LensFilterEvent } from '../types'; import { PieExpressionProps, PieExpressionArgs } from './types'; import { PieComponent } from './render_function'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; export interface PieRender { type: 'render'; @@ -93,8 +93,7 @@ export const pie: ExpressionFunctionDefinition< export const getPieRenderer = (dependencies: { formatFactory: Promise; - chartTheme: PartialTheme; - isDarkMode: boolean; + chartsThemeService: ChartsPluginSetup['theme']; }): ExpressionRenderDefinition => ({ name: 'lens_pie_renderer', displayName: i18n.translate('xpack.lens.pie.visualizationName', { @@ -116,10 +115,9 @@ export const getPieRenderer = (dependencies: { , domNode, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 2e29513ba548..cfbeb27efb3d 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -11,6 +11,9 @@ import { LensMultiTable } from '../types'; import { PieComponent } from './render_function'; import { PieExpressionArgs } from './types'; import { EmptyPlaceholder } from '../shared_components'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; + +const chartsThemeService = chartPluginMock.createSetupContract().theme; describe('PieVisualization component', () => { let getFormatSpy: jest.Mock; @@ -57,9 +60,8 @@ describe('PieVisualization component', () => { return { data, formatFactory: getFormatSpy, - isDarkMode: false, - chartTheme: {}, onClickValue: jest.fn(), + chartsThemeService, }; } diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 36e8d9660ab7..f349cc4dfd64 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -19,7 +19,6 @@ import { PartitionConfig, PartitionLayer, PartitionLayout, - PartialTheme, PartitionFillLabel, RecursivePartial, LayerValue, @@ -32,6 +31,7 @@ import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; import { desanitizeFilterContext } from '../utils'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; const EMPTY_SLICE = Symbol('empty_slice'); @@ -40,15 +40,14 @@ const sortedColors = euiPaletteColorBlindBehindText(); export function PieComponent( props: PieExpressionProps & { formatFactory: FormatFactory; - chartTheme: Exclude; - isDarkMode: boolean; + chartsThemeService: ChartsPluginSetup['theme']; onClickValue: (data: LensFilterEvent['data']) => void; } ) { const [firstTable] = Object.values(props.data.tables); const formatters: Record> = {}; - const { chartTheme, isDarkMode, onClickValue } = props; + const { chartsThemeService, onClickValue } = props; const { shape, groups, @@ -60,6 +59,9 @@ export function PieComponent( percentDecimals, hideLabels, } = props.args; + const isDarkMode = chartsThemeService.useDarkMode(); + const chartTheme = chartsThemeService.useChartsTheme(); + const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); if (!hideLabels) { firstTable.columns.forEach((column) => { @@ -245,6 +247,8 @@ export function PieComponent( onClickValue(desanitizeFilterContext(context)); }} + theme={chartTheme} + baseTheme={chartBaseTheme} /> , - { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies + { + kibanaLegacy, + expressions, + data, + embeddable, + visualizations, + charts, + }: LensPluginSetupDependencies ) { const editorFrameSetupInterface = this.editorFrameService.setup(core, { data, embeddable, expressions, }); - const dependencies = { + const dependencies: IndexPatternDatasourceSetupPlugins & + XyVisualizationPluginSetupPlugins & + DatatableVisualizationPluginSetupPlugins & + MetricVisualizationPluginSetupPlugins & + PieVisualizationPluginSetupPlugins = { expressions, data, + charts, editorFrame: editorFrameSetupInterface, formatFactory: core .getStartServices() diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index c037aecde558..d7d76bdd1f44 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -5,6 +5,9 @@ Object { "chain": Array [ Object { "arguments": Object { + "fittingFunction": Array [ + "Carry", + ], "layers": Array [ Object { "chain": Array [ diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap index 48c70e0a4a05..c7c173f87ad7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -5,6 +5,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` renderer="canvas" > ; editorFrame: EditorFrameSetup; + charts: ChartsPluginSetup; } function getTimeZone(uiSettings: IUiSettingsClient) { @@ -34,7 +35,7 @@ export class XyVisualization { setup( core: CoreSetup, - { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins + { expressions, formatFactory, editorFrame, charts }: XyVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => yAxisConfig); @@ -44,9 +45,7 @@ export class XyVisualization { expressions.registerRenderer( getXyChartRenderer({ formatFactory, - chartTheme: core.uiSettings.get('theme:darkMode') - ? EUI_CHARTS_THEME_DARK.theme - : EUI_CHARTS_THEME_LIGHT.theme, + chartsThemeService: charts.theme, timeZone: getTimeZone(core.uiSettings), histogramBarTarget: core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), }) diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index 6efcfcab1ff7..2ddb9418abad 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -5,7 +5,7 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { SeriesType, visualizationTypes } from './types'; +import { SeriesType, visualizationTypes, LayerConfig, YConfig } from './types'; export function isHorizontalSeries(seriesType: SeriesType) { return seriesType === 'bar_horizontal' || seriesType === 'bar_horizontal_stacked'; @@ -24,3 +24,12 @@ export function getIconForSeries(type: SeriesType): EuiIconType { return (definition.icon as EuiIconType) || 'empty'; } + +export const getSeriesColor = (layer: LayerConfig, accessor: string) => { + if (layer.splitAccessor) { + return null; + } + return ( + layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index e9e0cfed909f..31b34e41e82d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -40,6 +40,7 @@ describe('#toExpression', () => { { legend: { position: Position.Bottom, isVisible: true }, preferredSeriesType: 'bar', + fittingFunction: 'Carry', layers: [ { layerId: 'first', @@ -55,6 +56,27 @@ describe('#toExpression', () => { ).toMatchSnapshot(); }); + it('should default the fitting function to None', () => { + expect( + (xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + ) as Ast).chain[0].arguments.fittingFunction[0] + ).toEqual('None'); + }); + it('should not generate an expression when missing x', () => { expect( xyVisualization.toExpression( diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 6ec22270d8b1..3b9406cedd49 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -133,6 +133,7 @@ export const buildExpression = ( ], }, ], + fittingFunction: [state.fittingFunction || 'None'], layers: validLayers.map((layer) => { const columnToLabel: Record = {}; @@ -188,7 +189,8 @@ export const buildExpression = ( function: 'lens_xy_yConfig', arguments: { forAccessor: [yConfig.forAccessor], - axisMode: [yConfig.axisMode], + axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], + color: yConfig.color ? [yConfig.color] : [], }, }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index e62c5f60a58e..08f29c65b26d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -16,6 +16,7 @@ import chartBarHorizontalStackedSVG from '../assets/chart_bar_horizontal_stacked import chartLineSVG from '../assets/chart_line.svg'; import { VisualizationType } from '../index'; +import { FittingFunction } from './fitting_functions'; export interface LegendConfig { isVisible: boolean; @@ -100,6 +101,10 @@ export const yAxisConfig: ExpressionFunctionDefinition< options: ['auto', 'left', 'right'], help: 'The axis mode of the metric', }, + color: { + types: ['string'], + help: 'The color of the series', + }, }, fn: function fn(input: unknown, args: YConfig) { return { @@ -195,6 +200,7 @@ export type YAxisMode = 'auto' | 'left' | 'right'; export interface YConfig { forAccessor: string; axisMode?: YAxisMode; + color?: string; } export interface LayerConfig { @@ -220,12 +226,14 @@ export interface XYArgs { yTitle: string; legend: LegendConfig & { type: 'lens_xy_legendConfig' }; layers: LayerArgs[]; + fittingFunction?: FittingFunction; } // Persisted parts of the state export interface XYState { preferredSeriesType: SeriesType; legend: LegendConfig; + fittingFunction?: FittingFunction; layers: LayerConfig[]; } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss new file mode 100644 index 000000000000..c353f3f370ee --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss @@ -0,0 +1,3 @@ +.lnsXyToolbar__popover { + width: 400px; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 7544ed0f87b7..981ce1cca595 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -5,15 +5,15 @@ */ import React from 'react'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { EuiButtonGroupProps } from '@elastic/eui'; -import { LayerContextMenu } from './xy_config_panel'; +import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; +import { EuiButtonGroupProps, EuiSuperSelect } from '@elastic/eui'; +import { LayerContextMenu, XyToolbar } from './xy_config_panel'; import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; -describe('LayerContextMenu', () => { +describe('XY Config panels', () => { let frame: FramePublicAPI; function testState(): State { @@ -39,11 +39,6 @@ describe('LayerContextMenu', () => { }; }); - test.skip('allows toggling of legend visibility', () => {}); - test.skip('allows changing legend position', () => {}); - test.skip('allows toggling the y axis gridlines', () => {}); - test.skip('allows toggling the x axis gridlines', () => {}); - describe('LayerContextMenu', () => { test('enables stacked chart types even when there is no split series', () => { const state = testState(); @@ -92,4 +87,45 @@ describe('LayerContextMenu', () => { expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); }); + + describe('XyToolbar', () => { + it('should show currently selected fitting function', () => { + const state = testState(); + + const component = shallow( + + ); + + expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); + }); + + it('should disable the select if there is no unstacked area or line series', () => { + const state = testState(); + + const component = shallow( + + ); + + expect(component.find(EuiSuperSelect).prop('disabled')).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 3e73cd256bdb..84ea53fb4dc3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -4,13 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; +import { debounce } from 'lodash'; +import { + EuiButtonEmpty, + EuiButtonGroup, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, + EuiFormRow, + EuiPopover, + EuiText, + htmlIdGenerator, + EuiForm, + EuiColorPicker, + EuiColorPickerProps, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; +import { + VisualizationLayerWidgetProps, + VisualizationDimensionEditorProps, + VisualizationToolbarProps, +} from '../types'; import { State, SeriesType, visualizationTypes, YAxisMode } from './types'; -import { VisualizationDimensionEditorProps, VisualizationLayerWidgetProps } from '../types'; -import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; +import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; +import { fittingFunctionDefinitions } from './fitting_functions'; + +import './xy_config_panel.scss'; type UnwrapArray = T extends Array ? P : T; @@ -68,72 +91,247 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { ); } +export function XyToolbar(props: VisualizationToolbarProps) { + const [open, setOpen] = useState(false); + const hasNonBarSeries = props.state?.layers.some( + (layer) => layer.seriesType === 'line' || layer.seriesType === 'area' + ); + return ( + + + { + setOpen(!open); + }} + > + {i18n.translate('xpack.lens.xyChart.settingsLabel', { defaultMessage: 'Settings' })} + + } + isOpen={open} + closePopover={() => { + setOpen(false); + }} + anchorPosition="downRight" + > + + { + return { + value: id, + dropdownDisplay: ( + <> + {title} + +

{description}

+
+ + ), + inputDisplay: title, + }; + })} + valueOfSelected={props.state?.fittingFunction || 'None'} + onChange={(value) => props.setState({ ...props.state, fittingFunction: value })} + itemLayoutAlign="top" + hasDividers + /> +
+
+
+
+ ); +} const idPrefix = htmlIdGenerator()(); -export function DimensionEditor({ - state, - setState, - layerId, - accessor, -}: VisualizationDimensionEditorProps) { +export function DimensionEditor(props: VisualizationDimensionEditorProps) { + const { state, setState, layerId, accessor } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; const axisMode = (layer.yConfig && layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) || 'auto'; + return ( - - + + + { - const newMode = id.replace(idPrefix, '') as YAxisMode; - const newYAxisConfigs = [...(layer.yConfig || [])]; - const existingIndex = newYAxisConfigs.findIndex( - (yAxisConfig) => yAxisConfig.forAccessor === accessor - ); - if (existingIndex !== -1) { - newYAxisConfigs[existingIndex].axisMode = newMode; + > + { + const newMode = id.replace(idPrefix, '') as YAxisMode; + const newYAxisConfigs = [...(layer.yConfig || [])]; + const existingIndex = newYAxisConfigs.findIndex( + (yAxisConfig) => yAxisConfig.forAccessor === accessor + ); + if (existingIndex !== -1) { + newYAxisConfigs[existingIndex].axisMode = newMode; + } else { + newYAxisConfigs.push({ + forAccessor: accessor, + axisMode: newMode, + }); + } + setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index)); + }} + /> + + + ); +} + +const tooltipContent = { + auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', { + defaultMessage: 'Lens automatically picks colors for you unless you specify a custom color.', + }), + custom: i18n.translate('xpack.lens.configPanel.color.tooltip.custom', { + defaultMessage: 'Clear the custom color to return to “Auto” mode.', + }), + disabled: i18n.translate('xpack.lens.configPanel.color.tooltip.disabled', { + defaultMessage: + 'Individual series cannot be custom colored when the layer includes a “Break down by“', + }), +}; + +const ColorPicker = ({ + state, + setState, + layerId, + accessor, +}: VisualizationDimensionEditorProps) => { + const index = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[index]; + const disabled = !!layer.splitAccessor; + + const [color, setColor] = useState(getSeriesColor(layer, accessor)); + + const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { + setColor(text); + if (output.isValid || text === '') { + updateColorInState(text, output); + } + }; + + const updateColorInState: EuiColorPickerProps['onChange'] = React.useMemo( + () => + debounce((text, output) => { + const newYConfigs = [...(layer.yConfig || [])]; + const existingIndex = newYConfigs.findIndex((yConfig) => yConfig.forAccessor === accessor); + if (existingIndex !== -1) { + if (text === '') { + delete newYConfigs[existingIndex].color; } else { - newYAxisConfigs.push({ - forAccessor: accessor, - axisMode: newMode, - }); + newYConfigs[existingIndex].color = output.hex; } - setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index)); - }} - /> + } else { + newYConfigs.push({ + forAccessor: accessor, + color: output.hex, + }); + } + setState(updateLayer(state, { ...layer, yConfig: newYConfigs }, index)); + }, 256), + [state, layer, accessor, index] + ); + + return ( + + + {i18n.translate('xpack.lens.xyChart.seriesColor.label', { + defaultMessage: 'Series color', + })}{' '} + + + + } + > + {disabled ? ( + + + + ) : ( + + )} ); -} +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 34f2a9111253..b7a50b3af640 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -15,6 +15,7 @@ import { GeometryValue, XYChartSeriesIdentifier, SeriesNameFn, + Fit, } from '@elastic/charts'; import { xyChart, XYChart } from './xy_expression'; import { LensMultiTable } from '../types'; @@ -24,10 +25,13 @@ import { shallow } from 'enzyme'; import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; const onClickValue = jest.fn(); const onSelectRange = jest.fn(); +const chartsThemeService = chartPluginMock.createSetupContract().theme; + const dateHistogramData: LensMultiTable = { type: 'lens_multitable', tables: { @@ -324,7 +328,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -347,7 +351,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -398,7 +402,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -434,7 +438,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -471,7 +475,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -509,7 +513,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -554,7 +558,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -589,7 +593,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -606,7 +610,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -626,7 +630,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -646,7 +650,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -671,7 +675,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -721,7 +725,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -758,7 +762,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -778,7 +782,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -801,7 +805,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -822,7 +826,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="CEST" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -842,7 +846,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [firstLayer] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -869,7 +873,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -890,7 +894,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -993,6 +997,75 @@ describe('xy_expression', () => { }); }); + describe('y series coloring', () => { + test('color is applied to chart for multiple series', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['a', 'b'], + yConfig: [ + { + forAccessor: 'a', + color: '#550000', + }, + { + forAccessor: 'b', + color: '#FFFF00', + }, + ], + }, + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['c'], + yConfig: [ + { + forAccessor: 'c', + color: '#FEECDF', + }, + ], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + expect((component.find(LineSeries).at(0).prop('color') as Function)!()).toEqual('#550000'); + expect((component.find(LineSeries).at(1).prop('color') as Function)!()).toEqual('#FFFF00'); + expect((component.find(LineSeries).at(2).prop('color') as Function)!()).toEqual('#FEECDF'); + }); + test('color is not applied to chart when splitAccessor is defined or when yConfig is not configured', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + yConfig: [ + { + forAccessor: 'a', + color: '#550000', + }, + ], + }, + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['c'], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + expect((component.find(LineSeries).at(0).prop('color') as Function)!()).toEqual(null); + expect((component.find(LineSeries).at(1).prop('color') as Function)!()).toEqual(null); + }); + }); + describe('provides correct series naming', () => { const nameFnArgs = { seriesKeys: [], @@ -1196,7 +1269,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1215,7 +1288,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1234,7 +1307,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1252,7 +1325,7 @@ describe('xy_expression', () => { data={{ ...data }} args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} formatFactory={getFormatSpy} - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} timeZone="UTC" onClickValue={onClickValue} @@ -1274,7 +1347,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1359,7 +1432,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1417,7 +1490,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1473,7 +1546,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1482,5 +1555,66 @@ describe('xy_expression', () => { expect(component.find(Settings).prop('showLegend')).toEqual(true); }); + + test('it should apply the fitting function to all non-bar series', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: createSampleDatatableWithRows([ + { a: 1, b: 2, c: 'I', d: 'Foo' }, + { a: 1, b: 5, c: 'J', d: 'Bar' }, + ]), + }, + }; + + const args: XYArgs = createArgsWithLayers([ + { ...sampleLayer, accessors: ['a'] }, + { ...sampleLayer, seriesType: 'bar', accessors: ['a'] }, + { ...sampleLayer, seriesType: 'area', accessors: ['a'] }, + { ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'] }, + ]); + + const component = shallow( + + ); + + expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.Carry }); + expect(component.find(BarSeries).prop('fit')).toEqual(undefined); + expect(component.find(AreaSeries).at(0).prop('fit')).toEqual({ type: Fit.Carry }); + expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toEqual([]); + // stacked area series doesn't get the fit prop + expect(component.find(AreaSeries).at(1).prop('fit')).toEqual(undefined); + expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toEqual(['c']); + }); + + test('it should apply None fitting function if not specified', () => { + const { data, args } = sampleArgs(); + + args.layers[0].accessors = ['a']; + + const component = shallow( + + ); + + expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None }); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 17ed04aa0e9c..3ab12aa0879b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -15,7 +15,6 @@ import { AreaSeries, BarSeries, Position, - PartialTheme, GeometryValue, XYChartSeriesIdentifier, } from '@elastic/charts'; @@ -36,10 +35,12 @@ import { } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; -import { isHorizontalChart } from './state_helpers'; +import { isHorizontalChart, getSeriesColor } from './state_helpers'; import { parseInterval } from '../../../../../src/plugins/data/common'; +import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { EmptyPlaceholder } from '../shared_components'; import { desanitizeFilterContext } from '../utils'; +import { fittingFunctionDefinitions, getFitOptions } from './fitting_functions'; import { getAxesConfiguration } from './axes_configuration'; type InferPropType = T extends React.FunctionComponent ? P : T; @@ -59,7 +60,7 @@ export interface XYRender { } type XYChartRenderProps = XYChartProps & { - chartTheme: PartialTheme; + chartsThemeService: ChartsPluginSetup['theme']; formatFactory: FormatFactory; timeZone: string; histogramBarTarget: number; @@ -94,6 +95,13 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Configure the chart legend.', }), }, + fittingFunction: { + types: ['string'], + options: [...fittingFunctionDefinitions.map(({ id }) => id)], + help: i18n.translate('xpack.lens.xyChart.fittingFunction.help', { + defaultMessage: 'Define how missing values are treated', + }), + }, layers: { // eslint-disable-next-line @typescript-eslint/no-explicit-any types: ['lens_xy_layer'] as any, @@ -115,7 +123,7 @@ export const xyChart: ExpressionFunctionDefinition< export const getXyChartRenderer = (dependencies: { formatFactory: Promise; - chartTheme: PartialTheme; + chartsThemeService: ChartsPluginSetup['theme']; histogramBarTarget: number; timeZone: string; }): ExpressionRenderDefinition => ({ @@ -144,7 +152,7 @@ export const getXyChartRenderer = (dependencies: { { return !( @@ -276,6 +286,7 @@ export function XYChart({ legendPosition={legend.position} showLegendExtra={false} theme={chartTheme} + baseTheme={chartBaseTheme} tooltip={{ headerFormatter: (d) => xAxisFormatter.convert(d.value), }} @@ -430,6 +441,7 @@ export function XYChart({ data: rows, xScaleType, yScaleType, + color: () => getSeriesColor(layer, accessor), groupId: yAxesConfiguration.find((axisConfiguration) => axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor) )?.groupId, @@ -459,7 +471,7 @@ export function XYChart({ } // This handles both split and single-y cases: // * If split series without formatting, show the value literally - // * If single Y, the seriesKey will be the acccessor, so we show the human-readable name + // * If single Y, the seriesKey will be the accessor, so we show the human-readable name return splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? ''; }, }; @@ -468,17 +480,29 @@ export function XYChart({ switch (seriesType) { case 'line': - return ; + return ( + + ); case 'bar': case 'bar_stacked': case 'bar_horizontal': case 'bar_horizontal_stacked': return ; - default: + case 'area_stacked': return ; + case 'area': + return ( + + ); + default: + return assertNever(seriesType); } }) )} ); } + +function assertNever(x: never): never { + throw new Error('Unexpected series type: ' + x); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index c107d8d36824..f30120635506 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -331,6 +331,7 @@ describe('xy_suggestions', () => { test('makes a visible seriesType suggestion for unchanged table without split', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + fittingFunction: 'None', preferredSeriesType: 'bar', layers: [ { @@ -368,6 +369,7 @@ describe('xy_suggestions', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', + fittingFunction: 'None', layers: [ { accessors: ['price', 'quantity'], @@ -408,6 +410,7 @@ describe('xy_suggestions', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + fittingFunction: 'None', preferredSeriesType: 'bar', layers: [ { @@ -440,6 +443,7 @@ describe('xy_suggestions', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', + fittingFunction: 'None', layers: [ { accessors: ['price'], @@ -474,6 +478,7 @@ describe('xy_suggestions', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', + fittingFunction: 'None', layers: [ { accessors: ['price', 'quantity'], @@ -512,6 +517,7 @@ describe('xy_suggestions', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', + fittingFunction: 'None', layers: [ { accessors: ['price'], @@ -551,6 +557,7 @@ describe('xy_suggestions', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', + fittingFunction: 'None', layers: [ { accessors: ['price', 'quantity'], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 9d0ebbb389c0..e0bfbd266f8f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -402,6 +402,7 @@ function buildSuggestion({ const state: State = { legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, + fittingFunction: currentState?.fittingFunction || 'None', preferredSeriesType: seriesType, layers: [...keptLayers, newLayer], }; 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 474ea5c5b08c..f321e0962caa 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -11,7 +11,7 @@ import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; -import { DimensionEditor, LayerContextMenu } from './xy_config_panel'; +import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import { Visualization, OperationMetadata, VisualizationType } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; @@ -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'; } @@ -264,6 +264,15 @@ export const xyVisualization: Visualization = { ); }, + renderToolbar(domElement, props) { + render( + + + , + domElement + ); + }, + renderDimensionEditor(domElement, props) { render( 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 3a304e467e0c..7d541ec192e0 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 54f03605e14d..6bc57eae41b7 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 3f2debeebeb4..8ce04c83afdb 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 98c91fca1fcc..eeb197a58f51 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 86ace0e32cc8..e32b5f44c827 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 51e251a5d8e2..a0d2152e8866 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 e122d1cda3ed..d6f6ee8fa609 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 61ec02e72adf..96dad0c01139 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 97afac9ef174..e20c509ccd4a 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 b613f577067b..9431fb55dc88 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 076e7a758a4f..a4cff7c89a01 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 a95991271819..8c2f5e271ff5 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 7d39acd504c4..1859c7875ad1 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 550b3737963d..fac002d0a877 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 04a5381fa259..3cff48e4d682 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 a0dc07b8e545..a85cd0cc8640 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 44b7b2ffb6ae..42c19b8c641e 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 000000000000..0d4f1f99e464 --- /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 6c5a9af8f0f0..7d4d7bf3ec7a 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 000000000000..bcbeef29875e --- /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 1620e3058be6..1c48ed2290dc 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 0d2732184afc..000000000000 --- 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 000000000000..d2468496fbe0 --- /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 bc99285cfc7a..000000000000 --- 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 000000000000..33d684b32020 --- /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 557fe5fd5f70..71d76ff53d8a 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 376010f0df9b..e2050724ef68 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 000000000000..273611e94ee4 --- /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 000000000000..4752eeba2376 --- /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 a5934038f83d..e5801afd5b60 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 42235bfd5442..d96deb226744 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 fe974fa610c0..d2dd07b0f81f 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 0e29eca24464..5f57d666b9f7 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 11dc593a235a..65e914a1ac92 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/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 9539d530bab0..9d5125532e5b 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import ReactDOM from 'react-dom'; -import { AppMountParameters, CoreStart } from 'kibana/public'; +import { AppMountParameters, CoreStart, HttpStart } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; @@ -17,6 +17,8 @@ import { setLicenseCache } from './license'; import { MlSetupDependencies, MlStartDependencies } from '../plugin'; import { MlRouter } from './routing'; +import { mlApiServicesProvider } from './services/ml_api_service'; +import { HttpService } from './services/http_service'; type MlDependencies = MlSetupDependencies & MlStartDependencies; @@ -27,6 +29,23 @@ interface AppProps { const localStorage = new Storage(window.localStorage); +/** + * Provides global services available across the entire ML app. + */ +export function getMlGlobalServices(httpStart: HttpStart) { + const httpService = new HttpService(httpStart); + return { + httpService, + mlApiServices: mlApiServicesProvider(httpService), + }; +} + +export interface MlServicesContext { + mlServices: MlGlobalServices; +} + +export type MlGlobalServices = ReturnType; + const App: FC = ({ coreStart, deps }) => { const pageDeps = { indexPatterns: deps.data.indexPatterns, @@ -47,7 +66,9 @@ const App: FC = ({ coreStart, deps }) => { const I18nContext = coreStart.i18n.Context; return ( - + diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx index 3fb654f35be4..803281bcd0ce 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -27,7 +27,6 @@ import { normalizeTimes, } from './job_select_service_utils'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; -import { ml } from '../../services/ml_api_service'; import { useMlKibana } from '../../contexts/kibana'; import { JobSelectionMaps } from './job_selector'; @@ -66,7 +65,10 @@ export const JobSelectorFlyout: FC = ({ withTimeRangeSelector = true, }) => { const { - services: { notifications }, + services: { + notifications, + mlServices: { mlApiServices }, + }, } = useMlKibana(); const [newSelection, setNewSelection] = useState(selectedIds); @@ -151,7 +153,7 @@ export const JobSelectorFlyout: FC = ({ async function fetchJobs() { try { - const resp = await ml.jobs.jobsWithTimerange(dateFormatTz); + const resp = await mlApiServices.jobs.jobsWithTimerange(dateFormatTz); const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH); const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs); setJobs(normalizedJobs); diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx index 27f8c822d68e..beafae1ecd2f 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx @@ -9,10 +9,7 @@ import { Subscription } from 'rxjs'; import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui'; import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public'; -import { - mlTimefilterRefresh$, - mlTimefilterTimeChange$, -} from '../../../services/timefilter_refresh_service'; +import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { useUrlState } from '../../../util/url_state'; import { useMlKibana } from '../../../contexts/kibana'; @@ -108,7 +105,6 @@ export const DatePickerWrapper: FC = () => { timefilter.setTime(newTime); setTime(newTime); setRecentlyUsedRanges(getRecentlyUsedRanges()); - mlTimefilterTimeChange$.next({ lastRefresh: Date.now(), timeRange: { start, end } }); } function updateInterval({ diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 2a156b5716ad..3bc3b8c2c6df 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -13,6 +13,7 @@ import { import { SecurityPluginSetup } from '../../../../../security/public'; import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public'; import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; +import { MlServicesContext } from '../../app'; interface StartPlugins { data: DataPublicPluginStart; @@ -20,7 +21,8 @@ interface StartPlugins { licenseManagement?: LicenseManagementUIPluginSetup; share: SharePluginStart; } -export type StartServices = CoreStart & StartPlugins & { kibanaVersion: string }; +export type StartServices = CoreStart & + StartPlugins & { kibanaVersion: string } & MlServicesContext; // eslint-disable-next-line react-hooks/rules-of-hooks export const useMlKibana = () => useKibana(); export type MlKibanaReactContextValue = KibanaReactContextValue; diff --git a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts index 07d5a153664b..95ef5e5b2938 100644 --- a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts @@ -7,6 +7,7 @@ import React from 'react'; import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; import { SavedSearchSavedObject } from '../../../../common/types/kibana'; +import { MlServicesContext } from '../../app'; export interface MlContextValue { combinedQuery: any; @@ -34,4 +35,4 @@ export type SavedSearchQuery = object; // Multiple custom hooks can be created to access subsets of // the overall context value if necessary too, // see useCurrentIndexPattern() for example. -export const MlContext = React.createContext>({}); +export const MlContext = React.createContext>({}); 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 a423722d1447..5715687402bc 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.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index 7e5f354bbb40..63c471e66c49 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -1,3 +1,5 @@ +$borderRadius: $euiBorderRadius / 2; + .ml-swimlane-selector { visibility: hidden; } @@ -104,10 +106,9 @@ // SASSTODO: This entire selector needs to be rewritten. // It looks extremely brittle with very specific sizing units - .ml-explorer-swimlane { + .mlExplorerSwimlane { user-select: none; padding: 0; - margin-bottom: $euiSizeS; line.gridLine { stroke: $euiBorderColor; @@ -218,17 +219,20 @@ div.lane { height: 30px; border-bottom: 0px; - border-radius: 2px; - margin-top: -1px; + border-radius: $borderRadius; white-space: nowrap; + &:not(:first-child) { + margin-top: -1px; + } + div.lane-label { display: inline-block; - font-size: 13px; + font-size: $euiFontSizeXS; height: 30px; text-align: right; vertical-align: middle; - border-radius: 2px; + border-radius: $borderRadius; padding-right: 5px; margin-right: 5px; border: 1px solid transparent; @@ -261,7 +265,7 @@ .sl-cell-inner-dragselect { height: 26px; margin: 1px; - border-radius: 2px; + border-radius: $borderRadius; text-align: center; } @@ -293,7 +297,7 @@ .sl-cell-inner, .sl-cell-inner-dragselect { border: 2px solid $euiColorDarkShade; - border-radius: 2px; + border-radius: $borderRadius; opacity: 1; } } diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index 590a69283a81..095b42ffac5b 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -11,6 +11,7 @@ import useObservable from 'react-use/lib/useObservable'; import { forkJoin, of, Observable, Subject } from 'rxjs'; import { mergeMap, switchMap, tap } from 'rxjs/operators'; +import { useCallback, useMemo } from 'react'; import { anomalyDataChange } from '../explorer_charts/explorer_charts_container_service'; import { explorerService } from '../explorer_dashboard_service'; import { @@ -22,15 +23,17 @@ import { loadAnomaliesTableData, loadDataForCharts, loadFilteredTopInfluencers, - loadOverallData, loadTopInfluencers, - loadViewBySwimlane, - loadViewByTopFieldValuesForSelectedTime, AppStateSelectedCells, ExplorerJob, TimeRangeBounds, } from '../explorer_utils'; import { ExplorerState } from '../reducers'; +import { useMlKibana, useTimefilter } from '../../contexts/kibana'; +import { AnomalyTimelineService } from '../../services/anomaly_timeline_service'; +import { mlResultsServiceProvider } from '../../services/results_service'; +import { isViewBySwimLaneData } from '../swimlane_container'; +import { ANOMALY_SWIM_LANE_HARD_LIMIT } from '../explorer_constants'; // Memoize the data fetching methods. // wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument @@ -39,13 +42,13 @@ import { ExplorerState } from '../reducers'; // about this parameter. The generic type T retains and returns the type information of // the original function. const memoizeIsEqual = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs); -const wrapWithLastRefreshArg = any>(func: T) => { +const wrapWithLastRefreshArg = any>(func: T, context: any = null) => { return function (lastRefresh: number, ...args: Parameters): ReturnType { - return func.apply(null, args); + return func.apply(context, args); }; }; -const memoize = any>(func: T) => { - return memoizeOne(wrapWithLastRefreshArg(func), memoizeIsEqual); +const memoize = any>(func: T, context?: any) => { + return memoizeOne(wrapWithLastRefreshArg(func, context), memoizeIsEqual); }; const memoizedAnomalyDataChange = memoize(anomalyDataChange); @@ -56,9 +59,7 @@ const memoizedLoadDataForCharts = memoize(loadDataForC const memoizedLoadFilteredTopInfluencers = memoize( loadFilteredTopInfluencers ); -const memoizedLoadOverallData = memoize(loadOverallData); const memoizedLoadTopInfluencers = memoize(loadTopInfluencers); -const memoizedLoadViewBySwimlane = memoize(loadViewBySwimlane); const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData); export interface LoadExplorerDataConfig { @@ -73,6 +74,9 @@ export interface LoadExplorerDataConfig { tableInterval: string; tableSeverity: number; viewBySwimlaneFieldName: string; + viewByFromPage: number; + viewByPerPage: number; + swimlaneContainerWidth: number; } export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => { @@ -87,183 +91,213 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi /** * Fetches the data necessary for the Anomaly Explorer using observables. - * - * @param config LoadExplorerDataConfig - * - * @return Partial */ -function loadExplorerData(config: LoadExplorerDataConfig): Observable> { - if (!isLoadExplorerDataConfig(config)) { - return of({}); - } - - const { - bounds, - lastRefresh, - influencersFilterQuery, - noInfluencersConfigured, - selectedCells, - selectedJobs, - swimlaneBucketInterval, - swimlaneLimit, - tableInterval, - tableSeverity, - viewBySwimlaneFieldName, - } = config; - - const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName); - const jobIds = getSelectionJobIds(selectedCells, selectedJobs); - const timerange = getSelectionTimeRange( - selectedCells, - swimlaneBucketInterval.asSeconds(), - bounds +const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService) => { + const memoizedLoadOverallData = memoize( + anomalyTimelineService.loadOverallData, + anomalyTimelineService ); + const memoizedLoadViewBySwimlane = memoize( + anomalyTimelineService.loadViewBySwimlane, + anomalyTimelineService + ); + return (config: LoadExplorerDataConfig): Observable> => { + if (!isLoadExplorerDataConfig(config)) { + return of({}); + } - const dateFormatTz = getDateFormatTz(); - - // First get the data where we have all necessary args at hand using forkJoin: - // annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues - return forkJoin({ - annotationsData: memoizedLoadAnnotationsTableData( + const { + bounds, lastRefresh, + influencersFilterQuery, + noInfluencersConfigured, selectedCells, selectedJobs, + swimlaneBucketInterval, + swimlaneLimit, + tableInterval, + tableSeverity, + viewBySwimlaneFieldName, + swimlaneContainerWidth, + viewByFromPage, + viewByPerPage, + } = config; + + const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName); + const jobIds = getSelectionJobIds(selectedCells, selectedJobs); + const timerange = getSelectionTimeRange( + selectedCells, swimlaneBucketInterval.asSeconds(), bounds - ), - anomalyChartRecords: memoizedLoadDataForCharts( - lastRefresh, - jobIds, - timerange.earliestMs, - timerange.latestMs, - selectionInfluencers, - selectedCells, - influencersFilterQuery - ), - influencers: - selectionInfluencers.length === 0 - ? memoizedLoadTopInfluencers( + ); + + const dateFormatTz = getDateFormatTz(); + + // First get the data where we have all necessary args at hand using forkJoin: + // annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues + return forkJoin({ + annotationsData: memoizedLoadAnnotationsTableData( + lastRefresh, + selectedCells, + selectedJobs, + swimlaneBucketInterval.asSeconds(), + bounds + ), + anomalyChartRecords: memoizedLoadDataForCharts( + lastRefresh, + jobIds, + timerange.earliestMs, + timerange.latestMs, + selectionInfluencers, + selectedCells, + influencersFilterQuery + ), + influencers: + selectionInfluencers.length === 0 + ? memoizedLoadTopInfluencers( + lastRefresh, + jobIds, + timerange.earliestMs, + timerange.latestMs, + [], + noInfluencersConfigured, + influencersFilterQuery + ) + : Promise.resolve({}), + overallState: memoizedLoadOverallData(lastRefresh, selectedJobs, swimlaneContainerWidth), + tableData: memoizedLoadAnomaliesTableData( + lastRefresh, + selectedCells, + selectedJobs, + dateFormatTz, + swimlaneBucketInterval.asSeconds(), + bounds, + viewBySwimlaneFieldName, + tableInterval, + tableSeverity, + influencersFilterQuery + ), + topFieldValues: + selectedCells !== undefined && selectedCells.showTopFieldValues === true + ? anomalyTimelineService.loadViewByTopFieldValuesForSelectedTime( + timerange.earliestMs, + timerange.latestMs, + selectedJobs, + viewBySwimlaneFieldName, + swimlaneLimit, + viewByPerPage, + viewByFromPage, + swimlaneContainerWidth + ) + : Promise.resolve([]), + }).pipe( + // Trigger a side-effect action to reset view-by swimlane, + // show the view-by loading indicator + // and pass on the data we already fetched. + tap(explorerService.setViewBySwimlaneLoading), + // Trigger a side-effect to update the charts. + tap(({ anomalyChartRecords }) => { + if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) { + memoizedAnomalyDataChange( lastRefresh, - jobIds, + anomalyChartRecords, timerange.earliestMs, timerange.latestMs, + tableSeverity + ); + } else { + memoizedAnomalyDataChange( + lastRefresh, [], - noInfluencersConfigured, - influencersFilterQuery - ) - : Promise.resolve({}), - overallState: memoizedLoadOverallData( - lastRefresh, - selectedJobs, - swimlaneBucketInterval, - bounds - ), - tableData: memoizedLoadAnomaliesTableData( - lastRefresh, - selectedCells, - selectedJobs, - dateFormatTz, - swimlaneBucketInterval.asSeconds(), - bounds, - viewBySwimlaneFieldName, - tableInterval, - tableSeverity, - influencersFilterQuery - ), - topFieldValues: - selectedCells !== undefined && selectedCells.showTopFieldValues === true - ? loadViewByTopFieldValuesForSelectedTime( timerange.earliestMs, timerange.latestMs, - selectedJobs, - viewBySwimlaneFieldName, - swimlaneLimit, - noInfluencersConfigured - ) - : Promise.resolve([]), - }).pipe( - // Trigger a side-effect action to reset view-by swimlane, - // show the view-by loading indicator - // and pass on the data we already fetched. - tap(explorerService.setViewBySwimlaneLoading), - // Trigger a side-effect to update the charts. - tap(({ anomalyChartRecords }) => { - if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) { - memoizedAnomalyDataChange( - lastRefresh, - anomalyChartRecords, - timerange.earliestMs, - timerange.latestMs, - tableSeverity - ); - } else { - memoizedAnomalyDataChange( - lastRefresh, - [], - timerange.earliestMs, - timerange.latestMs, - tableSeverity - ); - } - }), - // Load view-by swimlane data and filtered top influencers. - // mergeMap is used to have access to the already fetched data and act on it in arg #1. - // In arg #2 of mergeMap we combine the data and pass it on in the action format - // which can be consumed by explorerReducer() later on. - mergeMap( - ({ anomalyChartRecords, influencers, overallState, topFieldValues }) => - forkJoin({ - influencers: - (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) && - anomalyChartRecords !== undefined && - anomalyChartRecords.length > 0 - ? memoizedLoadFilteredTopInfluencers( - lastRefresh, - jobIds, - timerange.earliestMs, - timerange.latestMs, - anomalyChartRecords, - selectionInfluencers, - noInfluencersConfigured, - influencersFilterQuery - ) - : Promise.resolve(influencers), - viewBySwimlaneState: memoizedLoadViewBySwimlane( - lastRefresh, - topFieldValues, - { - earliest: overallState.overallSwimlaneData.earliest, - latest: overallState.overallSwimlaneData.latest, - }, - selectedJobs, - viewBySwimlaneFieldName, - swimlaneLimit, - influencersFilterQuery, - noInfluencersConfigured - ), - }), - ( - { annotationsData, overallState, tableData }, - { influencers, viewBySwimlaneState } - ): Partial => { - return { - annotationsData, - influencers, - ...overallState, - ...viewBySwimlaneState, - tableData, - }; - } - ) - ); -} - -const loadExplorerData$ = new Subject(); -const explorerData$ = loadExplorerData$.pipe( - switchMap((config: LoadExplorerDataConfig) => loadExplorerData(config)) -); - + tableSeverity + ); + } + }), + // Load view-by swimlane data and filtered top influencers. + // mergeMap is used to have access to the already fetched data and act on it in arg #1. + // In arg #2 of mergeMap we combine the data and pass it on in the action format + // which can be consumed by explorerReducer() later on. + mergeMap( + ({ anomalyChartRecords, influencers, overallState, topFieldValues }) => + forkJoin({ + influencers: + (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) && + anomalyChartRecords !== undefined && + anomalyChartRecords.length > 0 + ? memoizedLoadFilteredTopInfluencers( + lastRefresh, + jobIds, + timerange.earliestMs, + timerange.latestMs, + anomalyChartRecords, + selectionInfluencers, + noInfluencersConfigured, + influencersFilterQuery + ) + : Promise.resolve(influencers), + viewBySwimlaneState: memoizedLoadViewBySwimlane( + lastRefresh, + topFieldValues, + { + earliest: overallState.earliest, + latest: overallState.latest, + }, + selectedJobs, + viewBySwimlaneFieldName, + ANOMALY_SWIM_LANE_HARD_LIMIT, + viewByPerPage, + viewByFromPage, + swimlaneContainerWidth, + influencersFilterQuery + ), + }), + ( + { annotationsData, overallState, tableData }, + { influencers, viewBySwimlaneState } + ): Partial => { + return { + annotationsData, + influencers, + loading: false, + viewBySwimlaneDataLoading: false, + overallSwimlaneData: overallState, + viewBySwimlaneData: viewBySwimlaneState, + tableData, + swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState) + ? viewBySwimlaneState.cardinality + : undefined, + }; + } + ) + ); + }; +}; export const useExplorerData = (): [Partial | undefined, (d: any) => void] => { + const timefilter = useTimefilter(); + + const { + services: { + mlServices: { mlApiServices }, + uiSettings, + }, + } = useMlKibana(); + const loadExplorerData = useMemo(() => { + const service = new AnomalyTimelineService( + timefilter, + uiSettings, + mlResultsServiceProvider(mlApiServices) + ); + return loadExplorerDataProvider(service); + }, []); + const loadExplorerData$ = useMemo(() => new Subject(), []); + const explorerData$ = useMemo(() => loadExplorerData$.pipe(switchMap(loadExplorerData)), []); const explorerData = useObservable(explorerData$); - return [explorerData, (c) => loadExplorerData$.next(c)]; + + const update = useCallback((c) => { + loadExplorerData$.next(c); + }, []); + + return [explorerData, update]; }; diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx index 16e2fb47a209..3ad749c9d063 100644 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx @@ -52,7 +52,6 @@ function getDefaultEmbeddablepaPanelConfig(jobIds: JobId[]) { interface AddToDashboardControlProps { jobIds: JobId[]; viewBy: string; - limit: number; onClose: (callback?: () => Promise) => void; } @@ -63,7 +62,6 @@ export const AddToDashboardControl: FC = ({ onClose, jobIds, viewBy, - limit, }) => { const { notifications: { toasts }, @@ -141,7 +139,6 @@ export const AddToDashboardControl: FC = ({ jobIds, swimlaneType, viewBy, - limit, }, }; } @@ -206,8 +203,8 @@ export const AddToDashboardControl: FC = ({ { id: SWIMLANE_TYPE.VIEW_BY, label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', { - defaultMessage: 'View by {viewByField}, up to {limit} rows', - values: { viewByField: viewBy, limit }, + defaultMessage: 'View by {viewByField}', + values: { viewByField: viewBy }, }), }, ]; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index b4d32e2af64b..e00e2e1e1e2e 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -22,12 +22,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DRAG_SELECT_ACTION, VIEW_BY_JOB_LABEL } from './explorer_constants'; +import { DRAG_SELECT_ACTION, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants'; import { AddToDashboardControl } from './add_to_dashboard_control'; import { useMlKibana } from '../contexts/kibana'; import { TimeBuckets } from '../util/time_buckets'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; -import { SelectLimit } from './select_limit'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$, @@ -36,9 +35,9 @@ import { import { ExplorerState } from './reducers/explorer_reducer'; import { hasMatchingPoints } from './has_matching_points'; import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers_found/explorer_no_influencers_found'; -import { LoadingIndicator } from '../components/loading_indicator'; import { SwimlaneContainer } from './swimlane_container'; -import { OverallSwimlaneData } from './explorer_utils'; +import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils'; +import { NoOverallData } from './components/no_overall_data'; function mapSwimlaneOptionsToEuiOptions(options: string[]) { return options.map((option) => ({ @@ -132,8 +131,11 @@ export const AnomalyTimeline: FC = React.memo( viewBySwimlaneDataLoading, viewBySwimlaneFieldName, viewBySwimlaneOptions, - swimlaneLimit, selectedJobs, + viewByFromPage, + viewByPerPage, + swimlaneLimit, + loading, } = explorerState; const setSwimlaneSelectActive = useCallback((active: boolean) => { @@ -159,25 +161,18 @@ export const AnomalyTimeline: FC = React.memo( }, []); // Listener for click events in the swimlane to load corresponding anomaly data. - const swimlaneCellClick = useCallback((selectedCellsUpdate: any) => { - // If selectedCells is an empty object we clear any existing selection, - // otherwise we save the new selection in AppState and update the Explorer. - if (Object.keys(selectedCellsUpdate).length === 0) { - setSelectedCells(); - } else { - setSelectedCells(selectedCellsUpdate); - } - }, []); - - const showOverallSwimlane = - overallSwimlaneData !== null && - overallSwimlaneData.laneLabels && - overallSwimlaneData.laneLabels.length > 0; - - const showViewBySwimlane = - viewBySwimlaneData !== null && - viewBySwimlaneData.laneLabels && - viewBySwimlaneData.laneLabels.length > 0; + const swimlaneCellClick = useCallback( + (selectedCellsUpdate: any) => { + // If selectedCells is an empty object we clear any existing selection, + // otherwise we save the new selection in AppState and update the Explorer. + if (Object.keys(selectedCellsUpdate).length === 0) { + setSelectedCells(); + } else { + setSelectedCells(selectedCellsUpdate); + } + }, + [setSelectedCells] + ); const menuItems = useMemo(() => { const items = []; @@ -235,21 +230,6 @@ export const AnomalyTimeline: FC = React.memo( /> - - - - - } - display={'columnCompressed'} - > - - -
{viewByLoadedForTimeFormatted && ( @@ -305,68 +285,84 @@ export const AnomalyTimeline: FC = React.memo(
- {showOverallSwimlane && ( - explorerService.setSwimlaneContainerWidth(width)} - /> - )} + explorerService.setSwimlaneContainerWidth(width)} + isLoading={loading} + noDataWarning={} + />
+ + {viewBySwimlaneOptions.length > 0 && ( <> - {showViewBySwimlane && ( - <> - -
- +
+ explorerService.setSwimlaneContainerWidth(width)} + fromPage={viewByFromPage} + perPage={viewByPerPage} + swimlaneLimit={swimlaneLimit} + onPaginationChange={({ perPage: perPageUpdate, fromPage: fromPageUpdate }) => { + if (perPageUpdate) { + explorerService.setViewByPerPage(perPageUpdate); } - timeBuckets={timeBuckets} - swimlaneCellClick={swimlaneCellClick} - swimlaneData={viewBySwimlaneData as OverallSwimlaneData} - swimlaneType={'viewBy'} - selection={selectedCells} - swimlaneRenderDoneListener={swimlaneRenderDoneListener} - onResize={(width) => explorerService.setSwimlaneContainerWidth(width)} - /> -
- - )} - - {viewBySwimlaneDataLoading && } - - {!showViewBySwimlane && - !viewBySwimlaneDataLoading && - typeof viewBySwimlaneFieldName === 'string' && ( - + ) : ( + + ) + ) : null + } /> - )} +
+ )} @@ -380,7 +376,6 @@ export const AnomalyTimeline: FC = React.memo( }} jobIds={selectedJobs.map(({ id }) => id)} viewBy={viewBySwimlaneFieldName!} - limit={swimlaneLimit} /> )} diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap index 3ba4ebb2acde..d3190d2ac1da 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap @@ -1,20 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ExplorerNoInfluencersFound snapshot 1`] = ` - - - + `; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx index 639c0f7b7850..24def0110858 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx @@ -7,7 +7,6 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt } from '@elastic/eui'; /* * React component for rendering EuiEmptyPrompt when no influencers were found. @@ -15,26 +14,17 @@ import { EuiEmptyPrompt } from '@elastic/eui'; export const ExplorerNoInfluencersFound: FC<{ viewBySwimlaneFieldName: string; showFilterMessage?: boolean; -}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) => ( - - {showFilterMessage === false && ( - - )} - {showFilterMessage === true && ( - - )} - - } - /> -); +}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) => + showFilterMessage === false ? ( + + ) : ( + + ); diff --git a/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx new file mode 100644 index 000000000000..e73aac66a0d9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const NoOverallData: FC = () => { + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 71c96840d1b5..df4cea0c0798 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -12,8 +12,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; import { EuiFlexGroup, @@ -27,6 +25,7 @@ import { EuiPageHeaderSection, EuiSpacer, EuiTitle, + EuiLoadingContent, } from '@elastic/eui'; import { AnnotationFlyout } from '../components/annotations/annotation_flyout'; @@ -36,12 +35,10 @@ import { DatePickerWrapper } from '../components/navigation_menu/date_picker_wra import { InfluencersList } from '../components/influencers_list'; import { explorerService } from './explorer_dashboard_service'; import { AnomalyResultsViewSelector } from '../components/anomaly_results_view_selector'; -import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; import { NavigationMenu } from '../components/navigation_menu'; import { CheckboxShowCharts } from '../components/controls/checkbox_showcharts'; import { JobSelector } from '../components/job_selector'; import { SelectInterval } from '../components/controls/select_interval/select_interval'; -import { limit$ } from './select_limit/select_limit'; import { SelectSeverity } from '../components/controls/select_severity/select_severity'; import { ExplorerQueryBar, @@ -142,19 +139,6 @@ export class Explorer extends React.Component { state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG }; - _unsubscribeAll = new Subject(); - - componentDidMount() { - limit$.pipe(takeUntil(this._unsubscribeAll)).subscribe(explorerService.setSwimlaneLimit); - } - - componentWillUnmount() { - this._unsubscribeAll.next(); - this._unsubscribeAll.complete(); - } - - viewByChangeHandler = (e) => explorerService.setViewBySwimlaneFieldName(e.target.value); - // Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes // and will cause a syntax error when called with getKqlQueryValues applyFilter = (fieldName, fieldValue, action) => { @@ -240,29 +224,7 @@ export class Explorer extends React.Component { const noJobsFound = selectedJobs === null || selectedJobs.length === 0; const hasResults = overallSwimlaneData.points && overallSwimlaneData.points.length > 0; - if (loading === true) { - return ( - - - - ); - } - - if (noJobsFound) { + if (noJobsFound && !loading) { return ( @@ -270,7 +232,7 @@ export class Explorer extends React.Component { ); } - if (noJobsFound && hasResults === false) { + if (noJobsFound && hasResults === false && !loading) { return ( @@ -320,7 +282,11 @@ export class Explorer extends React.Component { /> - + {loading ? ( + + ) : ( + + )}
)} @@ -352,59 +318,59 @@ export class Explorer extends React.Component { )} - -

- -

-
- - - - - - - - - + +

+ +

+
+ - -
-
- {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && ( - - - - - - )} -
- - - -
- {showCharts && } -
- - + + + + + + + + + + + {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && ( + + + + + + )} + + +
+ {showCharts && } +
+ + + )}
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 8b30dccc2530..898e29a30388 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/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index d1adf8c7ad74..21e13cb029d6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -27,9 +27,10 @@ export const EXPLORER_ACTION = { SET_INFLUENCER_FILTER_SETTINGS: 'setInfluencerFilterSettings', SET_SELECTED_CELLS: 'setSelectedCells', SET_SWIMLANE_CONTAINER_WIDTH: 'setSwimlaneContainerWidth', - SET_SWIMLANE_LIMIT: 'setSwimlaneLimit', SET_VIEW_BY_SWIMLANE_FIELD_NAME: 'setViewBySwimlaneFieldName', SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading', + SET_VIEW_BY_PER_PAGE: 'setViewByPerPage', + SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage', }; export const FILTER_ACTION = { @@ -51,9 +52,23 @@ export const CHART_TYPE = { }; export const MAX_CATEGORY_EXAMPLES = 10; + +/** + * Maximum amount of top influencer to fetch. + */ export const MAX_INFLUENCER_FIELD_VALUES = 10; export const MAX_INFLUENCER_FIELD_NAMES = 50; export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', { defaultMessage: 'job ID', }); +/** + * Hard limitation for the size of terms + * aggregations on influencers values. + */ +export const ANOMALY_SWIM_LANE_HARD_LIMIT = 1000; + +/** + * Default page size fot the anomaly swim lane. + */ +export const SWIM_LANE_DEFAULT_PAGE_SIZE = 10; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 30ab918983a7..1429bf085836 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -12,7 +12,7 @@ import { isEqual } from 'lodash'; import { from, isObservable, Observable, Subject } from 'rxjs'; -import { distinctUntilChanged, flatMap, map, scan } from 'rxjs/operators'; +import { distinctUntilChanged, flatMap, map, scan, shareReplay } from 'rxjs/operators'; import { DeepPartial } from '../../../common/types/common'; @@ -49,7 +49,9 @@ const explorerFilteredAction$ = explorerAction$.pipe( // applies action and returns state const explorerState$: Observable = explorerFilteredAction$.pipe( - scan(explorerReducer, getExplorerDefaultState()) + scan(explorerReducer, getExplorerDefaultState()), + // share the last emitted value among new subscribers + shareReplay(1) ); interface ExplorerAppState { @@ -59,6 +61,8 @@ interface ExplorerAppState { selectedTimes?: number[]; showTopFieldValues?: boolean; viewByFieldName?: string; + viewByPerPage?: number; + viewByFromPage?: number; }; mlExplorerFilter: { influencersFilterQuery?: unknown; @@ -88,6 +92,14 @@ const explorerAppState$: Observable = explorerState$.pipe( appState.mlExplorerSwimlane.viewByFieldName = state.viewBySwimlaneFieldName; } + if (state.viewByFromPage !== undefined) { + appState.mlExplorerSwimlane.viewByFromPage = state.viewByFromPage; + } + + if (state.viewByPerPage !== undefined) { + appState.mlExplorerSwimlane.viewByPerPage = state.viewByPerPage; + } + if (state.filterActive) { appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery; appState.mlExplorerFilter.filterActive = state.filterActive; @@ -153,13 +165,16 @@ export const explorerService = { payload, }); }, - setSwimlaneLimit: (payload: number) => { - explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_LIMIT, payload }); - }, setViewBySwimlaneFieldName: (payload: string) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME, payload }); }, setViewBySwimlaneLoading: (payload: any) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_LOADING, payload }); }, + setViewByFromPage: (payload: number) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE, payload }); + }, + setViewByPerPage: (payload: number) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE, payload }); + }, }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx index 4e6dcdcc5129..aa386288ac7e 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx @@ -29,7 +29,7 @@ import { ChartTooltipService, ChartTooltipValue, } from '../components/chart_tooltip/chart_tooltip_service'; -import { OverallSwimlaneData } from './explorer_utils'; +import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils'; const SCSS = { mlDragselectDragging: 'mlDragselectDragging', @@ -57,7 +57,7 @@ export interface ExplorerSwimlaneProps { maskAll?: boolean; timeBuckets: InstanceType; swimlaneCellClick?: Function; - swimlaneData: OverallSwimlaneData; + swimlaneData: OverallSwimlaneData | ViewBySwimLaneData; swimlaneType: SwimlaneType; selection?: { lanes: any[]; @@ -211,7 +211,7 @@ export class ExplorerSwimlane extends React.Component { const { swimlaneType } = this.props; // This selects both overall and viewby swimlane - const wrapper = d3.selectAll('.ml-explorer-swimlane'); + const wrapper = d3.selectAll('.mlExplorerSwimlane'); wrapper.selectAll('.lane-label').classed('lane-label-masked', true); wrapper @@ -242,7 +242,7 @@ export class ExplorerSwimlane extends React.Component { maskIrrelevantSwimlanes(maskAll: boolean) { if (maskAll === true) { // This selects both overall and viewby swimlane - const allSwimlanes = d3.selectAll('.ml-explorer-swimlane'); + const allSwimlanes = d3.selectAll('.mlExplorerSwimlane'); allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true); allSwimlanes .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') @@ -258,7 +258,7 @@ export class ExplorerSwimlane extends React.Component { clearSelection() { // This selects both overall and viewby swimlane - const wrapper = d3.selectAll('.ml-explorer-swimlane'); + const wrapper = d3.selectAll('.mlExplorerSwimlane'); wrapper.selectAll('.lane-label').classed('lane-label-masked', false); wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts index 2d49fa737cef..05fdb52e1ccb 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts @@ -8,8 +8,6 @@ import { Moment } from 'moment'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; -import { TimeBucketsInterval } from '../util/time_buckets'; - interface ClearedSelectedAnomaliesState { selectedCells: undefined; viewByLoadedForTimeFormatted: null; @@ -35,6 +33,10 @@ export declare interface OverallSwimlaneData extends SwimlaneData { latest: number; } +export interface ViewBySwimLaneData extends OverallSwimlaneData { + cardinality: number; +} + export declare const getDateFormatTz: () => any; export declare const getDefaultSwimlaneData: () => SwimlaneData; @@ -163,22 +165,6 @@ declare interface LoadOverallDataResponse { overallSwimlaneData: OverallSwimlaneData; } -export declare const loadOverallData: ( - selectedJobs: ExplorerJob[], - interval: TimeBucketsInterval, - bounds: TimeRangeBounds -) => Promise; - -export declare const loadViewBySwimlane: ( - fieldValues: string[], - bounds: SwimlaneBounds, - selectedJobs: ExplorerJob[], - viewBySwimlaneFieldName: string, - swimlaneLimit: number, - influencersFilterQuery: any, - noInfluencersConfigured: boolean -) => Promise; - export declare const loadViewByTopFieldValuesForSelectedTime: ( earliestMs: number, latestMs: number, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index bd6a7ee59c94..23da9669ee9a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -8,11 +8,9 @@ * utils for Anomaly Explorer. */ -import { chain, each, get, union, uniq } from 'lodash'; +import { chain, get, union, uniq } from 'lodash'; import moment from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; - import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, @@ -27,7 +25,7 @@ import { parseInterval } from '../../../common/util/parse_interval'; import { ml } from '../services/ml_api_service'; import { mlJobService } from '../services/job_service'; import { mlResultsService } from '../services/results_service'; -import { getBoundsRoundedToInterval, getTimeBucketsFromCache } from '../util/time_buckets'; +import { getTimeBucketsFromCache } from '../util/time_buckets'; import { getTimefilter, getUiSettings } from '../util/dependency_cache'; import { @@ -36,7 +34,6 @@ import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; -import { getSwimlaneContainerWidth } from './legacy_utils'; // create new job objects based on standard job config objects // new job objects just contain job id, bucket span in seconds and a selected flag. @@ -51,6 +48,7 @@ export function getClearedSelectedAnomaliesState() { return { selectedCells: undefined, viewByLoadedForTimeFormatted: null, + swimlaneLimit: undefined, }; } @@ -267,58 +265,6 @@ export function getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth) return buckets.getInterval(); } -export function loadViewByTopFieldValuesForSelectedTime( - earliestMs, - latestMs, - selectedJobs, - viewBySwimlaneFieldName, - swimlaneLimit, - noInfluencersConfigured -) { - const selectedJobIds = selectedJobs.map((d) => d.id); - - // Find the top field values for the selected time, and then load the 'view by' - // swimlane over the full time range for those specific field values. - return new Promise((resolve) => { - if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) { - mlResultsService - .getTopInfluencers(selectedJobIds, earliestMs, latestMs, swimlaneLimit) - .then((resp) => { - if (resp.influencers[viewBySwimlaneFieldName] === undefined) { - resolve([]); - } - - const topFieldValues = []; - const topInfluencers = resp.influencers[viewBySwimlaneFieldName]; - if (Array.isArray(topInfluencers)) { - topInfluencers.forEach((influencerData) => { - if (influencerData.maxAnomalyScore > 0) { - topFieldValues.push(influencerData.influencerFieldValue); - } - }); - } - resolve(topFieldValues); - }); - } else { - mlResultsService - .getScoresByBucket( - selectedJobIds, - earliestMs, - latestMs, - getSwimlaneBucketInterval( - selectedJobs, - getSwimlaneContainerWidth(noInfluencersConfigured) - ).asSeconds() + 's', - swimlaneLimit - ) - .then((resp) => { - const topFieldValues = Object.keys(resp.results); - resolve(topFieldValues); - }); - } - }); -} - // Obtain the list of 'View by' fields per job and viewBySwimlaneFieldName export function getViewBySwimlaneOptions({ currentViewBySwimlaneFieldName, @@ -435,105 +381,6 @@ export function getViewBySwimlaneOptions({ }; } -export function processOverallResults(scoresByTime, searchBounds, interval) { - const overallLabel = i18n.translate('xpack.ml.explorer.overallLabel', { - defaultMessage: 'Overall', - }); - const dataset = { - laneLabels: [overallLabel], - points: [], - interval, - earliest: searchBounds.min.valueOf() / 1000, - latest: searchBounds.max.valueOf() / 1000, - }; - - if (Object.keys(scoresByTime).length > 0) { - // Store the earliest and latest times of the data returned by the ES aggregations, - // These will be used for calculating the earliest and latest times for the swimlane charts. - each(scoresByTime, (score, timeMs) => { - const time = timeMs / 1000; - dataset.points.push({ - laneLabel: overallLabel, - time, - value: score, - }); - - dataset.earliest = Math.min(time, dataset.earliest); - dataset.latest = Math.max(time + dataset.interval, dataset.latest); - }); - } - - return dataset; -} - -export function processViewByResults( - scoresByInfluencerAndTime, - sortedLaneValues, - bounds, - viewBySwimlaneFieldName, - interval -) { - // Processes the scores for the 'view by' swimlane. - // Sorts the lanes according to the supplied array of lane - // values in the order in which they should be displayed, - // or pass an empty array to sort lanes according to max score over all time. - const dataset = { - fieldName: viewBySwimlaneFieldName, - points: [], - interval, - }; - - // Set the earliest and latest to be the same as the overall swimlane. - dataset.earliest = bounds.earliest; - dataset.latest = bounds.latest; - - const laneLabels = []; - const maxScoreByLaneLabel = {}; - - each(scoresByInfluencerAndTime, (influencerData, influencerFieldValue) => { - laneLabels.push(influencerFieldValue); - maxScoreByLaneLabel[influencerFieldValue] = 0; - - each(influencerData, (anomalyScore, timeMs) => { - const time = timeMs / 1000; - dataset.points.push({ - laneLabel: influencerFieldValue, - time, - value: anomalyScore, - }); - maxScoreByLaneLabel[influencerFieldValue] = Math.max( - maxScoreByLaneLabel[influencerFieldValue], - anomalyScore - ); - }); - }); - - const sortValuesLength = sortedLaneValues.length; - if (sortValuesLength === 0) { - // Sort lanes in descending order of max score. - // Note the keys in scoresByInfluencerAndTime received from the ES request - // are not guaranteed to be sorted by score if they can be parsed as numbers - // (e.g. if viewing by HTTP response code). - dataset.laneLabels = laneLabels.sort((a, b) => { - return maxScoreByLaneLabel[b] - maxScoreByLaneLabel[a]; - }); - } else { - // Sort lanes according to supplied order - // e.g. when a cell in the overall swimlane has been selected. - // Find the index of each lane label from the actual data set, - // rather than using sortedLaneValues as-is, just in case they differ. - dataset.laneLabels = laneLabels.sort((a, b) => { - let aIndex = sortedLaneValues.indexOf(a); - let bIndex = sortedLaneValues.indexOf(b); - aIndex = aIndex > -1 ? aIndex : sortValuesLength; - bIndex = bIndex > -1 ? bIndex : sortValuesLength; - return aIndex - bIndex; - }); - } - - return dataset; -} - export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, bounds) { const jobIds = selectedCells !== undefined && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL @@ -723,138 +570,6 @@ export async function loadDataForCharts( }); } -export function loadOverallData(selectedJobs, interval, bounds) { - return new Promise((resolve) => { - // Loads the overall data components i.e. the overall swimlane and influencers list. - if (selectedJobs === null) { - resolve({ - loading: false, - hasResuts: false, - }); - return; - } - - // Ensure the search bounds align to the bucketing interval used in the swimlane so - // that the first and last buckets are complete. - const searchBounds = getBoundsRoundedToInterval(bounds, interval, false); - const selectedJobIds = selectedJobs.map((d) => d.id); - - // Load the overall bucket scores by time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets - // which wouldn't be the case if e.g. '1M' was used. - // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works - // to ensure the search is inclusive of end time. - const overallBucketsBounds = getBoundsRoundedToInterval(bounds, interval, true); - mlResultsService - .getOverallBucketScores( - selectedJobIds, - // Note there is an optimization for when top_n == 1. - // If top_n > 1, we should test what happens when the request takes long - // and refactor the loading calls, if necessary, to avoid delays in loading other components. - 1, - overallBucketsBounds.min.valueOf(), - overallBucketsBounds.max.valueOf(), - interval.asSeconds() + 's' - ) - .then((resp) => { - const overallSwimlaneData = processOverallResults( - resp.results, - searchBounds, - interval.asSeconds() - ); - - resolve({ - loading: false, - overallSwimlaneData, - }); - }); - }); -} - -export function loadViewBySwimlane( - fieldValues, - bounds, - selectedJobs, - viewBySwimlaneFieldName, - swimlaneLimit, - influencersFilterQuery, - noInfluencersConfigured -) { - return new Promise((resolve) => { - const finish = (resp) => { - if (resp !== undefined) { - const viewBySwimlaneData = processViewByResults( - resp.results, - fieldValues, - bounds, - viewBySwimlaneFieldName, - getSwimlaneBucketInterval( - selectedJobs, - getSwimlaneContainerWidth(noInfluencersConfigured) - ).asSeconds() - ); - - resolve({ - viewBySwimlaneData, - viewBySwimlaneDataLoading: false, - }); - } else { - resolve({ viewBySwimlaneDataLoading: false }); - } - }; - - if (selectedJobs === undefined || viewBySwimlaneFieldName === undefined) { - finish(); - return; - } else { - // Ensure the search bounds align to the bucketing interval used in the swimlane so - // that the first and last buckets are complete. - const timefilter = getTimefilter(); - const timefilterBounds = timefilter.getActiveBounds(); - const searchBounds = getBoundsRoundedToInterval( - timefilterBounds, - getSwimlaneBucketInterval(selectedJobs, getSwimlaneContainerWidth(noInfluencersConfigured)), - false - ); - const selectedJobIds = selectedJobs.map((d) => d.id); - - // load scores by influencer/jobId value and time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets - // which wouldn't be the case if e.g. '1M' was used. - const interval = `${getSwimlaneBucketInterval( - selectedJobs, - getSwimlaneContainerWidth(noInfluencersConfigured) - ).asSeconds()}s`; - if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) { - mlResultsService - .getInfluencerValueMaxScoreByTime( - selectedJobIds, - viewBySwimlaneFieldName, - fieldValues, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit, - influencersFilterQuery - ) - .then(finish); - } else { - const jobIds = - fieldValues !== undefined && fieldValues.length > 0 ? fieldValues : selectedJobIds; - mlResultsService - .getScoresByBucket( - jobIds, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit - ) - .then(finish); - } - } - }); -} - export async function loadTopInfluencers( selectedJobIds, earliestMs, @@ -871,6 +586,8 @@ export async function loadTopInfluencers( earliestMs, latestMs, MAX_INFLUENCER_FIELD_VALUES, + 10, + 1, influencers, influencersFilterQuery ) diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index a19750494afd..068f43a140c9 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { useCallback, useMemo } from 'react'; import { useUrlState } from '../../util/url_state'; import { SWIMLANE_TYPE } from '../explorer_constants'; import { AppStateSelectedCells } from '../explorer_utils'; @@ -14,55 +15,55 @@ export const useSelectedCells = (): [ ] => { const [appState, setAppState] = useUrlState('_a'); - let selectedCells: AppStateSelectedCells | undefined; - // keep swimlane selection, restore selectedCells from AppState - if ( - appState && - appState.mlExplorerSwimlane && - appState.mlExplorerSwimlane.selectedType !== undefined - ) { - selectedCells = { - type: appState.mlExplorerSwimlane.selectedType, - lanes: appState.mlExplorerSwimlane.selectedLanes, - times: appState.mlExplorerSwimlane.selectedTimes, - showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues, - viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName, - }; - } + const selectedCells = useMemo(() => { + return appState?.mlExplorerSwimlane?.selectedType !== undefined + ? { + type: appState.mlExplorerSwimlane.selectedType, + lanes: appState.mlExplorerSwimlane.selectedLanes, + times: appState.mlExplorerSwimlane.selectedTimes, + showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues, + viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName, + } + : undefined; + // TODO fix appState to use memoization + }, [JSON.stringify(appState?.mlExplorerSwimlane)]); - const setSelectedCells = (swimlaneSelectedCells: AppStateSelectedCells) => { - const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane }; + const setSelectedCells = useCallback( + (swimlaneSelectedCells: AppStateSelectedCells) => { + const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane }; - if (swimlaneSelectedCells !== undefined) { - swimlaneSelectedCells.showTopFieldValues = false; + if (swimlaneSelectedCells !== undefined) { + swimlaneSelectedCells.showTopFieldValues = false; - const currentSwimlaneType = selectedCells?.type; - const currentShowTopFieldValues = selectedCells?.showTopFieldValues; - const newSwimlaneType = swimlaneSelectedCells?.type; + const currentSwimlaneType = selectedCells?.type; + const currentShowTopFieldValues = selectedCells?.showTopFieldValues; + const newSwimlaneType = swimlaneSelectedCells?.type; - if ( - (currentSwimlaneType === SWIMLANE_TYPE.OVERALL && - newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) || - newSwimlaneType === SWIMLANE_TYPE.OVERALL || - currentShowTopFieldValues === true - ) { - swimlaneSelectedCells.showTopFieldValues = true; - } + if ( + (currentSwimlaneType === SWIMLANE_TYPE.OVERALL && + newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) || + newSwimlaneType === SWIMLANE_TYPE.OVERALL || + currentShowTopFieldValues === true + ) { + swimlaneSelectedCells.showTopFieldValues = true; + } - mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type; - mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes; - mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times; - mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues; - setAppState('mlExplorerSwimlane', mlExplorerSwimlane); - } else { - delete mlExplorerSwimlane.selectedType; - delete mlExplorerSwimlane.selectedLanes; - delete mlExplorerSwimlane.selectedTimes; - delete mlExplorerSwimlane.showTopFieldValues; - setAppState('mlExplorerSwimlane', mlExplorerSwimlane); - } - }; + mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type; + mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes; + mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times; + mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues; + setAppState('mlExplorerSwimlane', mlExplorerSwimlane); + } else { + delete mlExplorerSwimlane.selectedType; + delete mlExplorerSwimlane.selectedLanes; + delete mlExplorerSwimlane.selectedTimes; + delete mlExplorerSwimlane.showTopFieldValues; + setAppState('mlExplorerSwimlane', mlExplorerSwimlane); + } + }, + [appState?.mlExplorerSwimlane, selectedCells] + ); return [selectedCells, setSelectedCells]; }; diff --git a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts b/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts index 3b92ee3fa37f..b85b0401c45c 100644 --- a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts @@ -11,8 +11,3 @@ export function getChartContainerWidth() { const chartContainer = document.querySelector('.explorer-charts'); return Math.floor((chartContainer && chartContainer.clientWidth) || 0); } - -export function getSwimlaneContainerWidth() { - const explorerContainer = document.querySelector('.ml-explorer'); - return (explorerContainer && explorerContainer.clientWidth) || 0; -} diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts index 1614da14e355..dd1d0516b617 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts @@ -19,5 +19,6 @@ export function clearInfluencerFilterSettings(state: ExplorerState): ExplorerSta queryString: '', tableQueryString: '', ...getClearedSelectedAnomaliesState(), + viewByFromPage: 1, }; } diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts index a26c0564c6b1..49f5794273a0 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts @@ -17,6 +17,7 @@ export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload) noInfluencersConfigured: getInfluencers(selectedJobs).length === 0, overallSwimlaneData: getDefaultSwimlaneData(), selectedJobs, + viewByFromPage: 1, }; // clear filter if selected jobs have no influencers diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index c31b26b7adb7..c55c06c80ab8 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -27,7 +27,7 @@ import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder'; export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => { const { type, payload } = nextAction; - let nextState; + let nextState: ExplorerState; switch (type) { case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS: @@ -39,6 +39,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo ...state, ...getClearedSelectedAnomaliesState(), loading: false, + viewByFromPage: 1, selectedJobs: [], }; break; @@ -82,22 +83,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo break; case EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH: - if (state.noInfluencersConfigured === true) { - // swimlane is full width, minus 30 for the 'no influencers' info icon, - // minus 170 for the lane labels, minus 50 padding - nextState = { ...state, swimlaneContainerWidth: payload - 250 }; - } else { - // swimlane width is 5 sixths of the window, - // minus 170 for the lane labels, minus 50 padding - nextState = { ...state, swimlaneContainerWidth: (payload / 6) * 5 - 220 }; - } - break; - - case EXPLORER_ACTION.SET_SWIMLANE_LIMIT: - nextState = { - ...state, - swimlaneLimit: payload, - }; + nextState = { ...state, swimlaneContainerWidth: payload }; break; case EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME: @@ -117,6 +103,9 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo ...getClearedSelectedAnomaliesState(), maskAll, viewBySwimlaneFieldName, + viewBySwimlaneData: getDefaultSwimlaneData(), + viewByFromPage: 1, + viewBySwimlaneDataLoading: true, }; break; @@ -125,7 +114,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo nextState = { ...state, annotationsData, - ...overallState, + overallSwimlaneData: overallState, tableData, viewBySwimlaneData: { ...getDefaultSwimlaneData(), @@ -134,6 +123,22 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo }; break; + case EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE: + nextState = { + ...state, + viewByFromPage: payload, + }; + break; + + case EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE: + nextState = { + ...state, + // reset current page on the page size change + viewByFromPage: 1, + viewByPerPage: payload, + }; + break; + default: nextState = state; } @@ -155,7 +160,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo filteredFields: nextState.filteredFields, isAndOperator: nextState.isAndOperator, selectedJobs: nextState.selectedJobs, - selectedCells: nextState.selectedCells, + selectedCells: nextState.selectedCells!, }); const { bounds, selectedCells } = nextState; diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts index 819f6ca1cac9..be87de7da8c8 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts @@ -57,5 +57,6 @@ export function setInfluencerFilterSettings( filteredFields.includes(selectedViewByFieldName) === false, viewBySwimlaneFieldName: selectedViewByFieldName, viewBySwimlaneOptions: filteredViewBySwimlaneOptions, + viewByFromPage: 1, }; } diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index 4e1a2af9b13a..892b46467345 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -19,7 +19,9 @@ import { TimeRangeBounds, OverallSwimlaneData, SwimlaneData, + ViewBySwimLaneData, } from '../../explorer_utils'; +import { SWIM_LANE_DEFAULT_PAGE_SIZE } from '../../explorer_constants'; export interface ExplorerState { annotationsData: any[]; @@ -42,14 +44,16 @@ export interface ExplorerState { selectedJobs: ExplorerJob[] | null; swimlaneBucketInterval: any; swimlaneContainerWidth: number; - swimlaneLimit: number; tableData: AnomaliesTableData; tableQueryString: string; viewByLoadedForTimeFormatted: string | null; - viewBySwimlaneData: SwimlaneData | OverallSwimlaneData; + viewBySwimlaneData: SwimlaneData | ViewBySwimLaneData; viewBySwimlaneDataLoading: boolean; viewBySwimlaneFieldName?: string; + viewByPerPage: number; + viewByFromPage: number; viewBySwimlaneOptions: string[]; + swimlaneLimit?: number; } function getDefaultIndexPattern() { @@ -78,7 +82,6 @@ export function getExplorerDefaultState(): ExplorerState { selectedJobs: null, swimlaneBucketInterval: undefined, swimlaneContainerWidth: 0, - swimlaneLimit: 10, tableData: { anomalies: [], examplesByJobId: [''], @@ -92,5 +95,8 @@ export function getExplorerDefaultState(): ExplorerState { viewBySwimlaneDataLoading: false, viewBySwimlaneFieldName: undefined, viewBySwimlaneOptions: [], + viewByPerPage: SWIM_LANE_DEFAULT_PAGE_SIZE, + viewByFromPage: 1, + swimlaneLimit: undefined, }; } diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx deleted file mode 100644 index cf65419e4bd8..000000000000 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx +++ /dev/null @@ -1,29 +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 { act } from 'react-dom/test-utils'; -import { shallow } from 'enzyme'; -import { SelectLimit } from './select_limit'; - -describe('SelectLimit', () => { - test('creates correct initial selected value', () => { - const wrapper = shallow(); - expect(wrapper.props().value).toEqual(10); - }); - - test('state for currently selected value is updated correctly on click', () => { - const wrapper = shallow(); - expect(wrapper.props().value).toEqual(10); - - act(() => { - wrapper.simulate('change', { target: { value: 25 } }); - }); - wrapper.update(); - - expect(wrapper.props().value).toEqual(10); - }); -}); diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx deleted file mode 100644 index 7a2df1a0f053..000000000000 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx +++ /dev/null @@ -1,40 +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. - */ - -/* - * React component for rendering a select element with limit options. - */ -import React from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject } from 'rxjs'; - -import { EuiSelect } from '@elastic/eui'; - -const limitOptions = [5, 10, 25, 50]; - -const euiOptions = limitOptions.map((limit) => ({ - value: limit, - text: `${limit}`, -})); - -export const defaultLimit = limitOptions[1]; -export const limit$ = new BehaviorSubject(defaultLimit); - -export const useSwimlaneLimit = (): [number, (newLimit: number) => void] => { - const limit = useObservable(limit$, defaultLimit); - - return [limit!, (newLimit: number) => limit$.next(newLimit)]; -}; - -export const SelectLimit = () => { - const [limit, setLimit] = useSwimlaneLimit(); - - function onChange(e: React.ChangeEvent) { - setLimit(parseInt(e.target.value, 10)); - } - - return ; -}; diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 57d1fd81000b..e34e1d26c9ca 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -5,7 +5,14 @@ */ import React, { FC, useCallback, useState } from 'react'; -import { EuiResizeObserver, EuiText } from '@elastic/eui'; +import { + EuiText, + EuiLoadingChart, + EuiResizeObserver, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, +} from '@elastic/eui'; import { throttle } from 'lodash'; import { @@ -14,48 +21,139 @@ import { } from '../../application/explorer/explorer_swimlane'; import { MlTooltipComponent } from '../../application/components/chart_tooltip'; +import { SwimLanePagination } from './swimlane_pagination'; +import { SWIMLANE_TYPE } from './explorer_constants'; +import { ViewBySwimLaneData } from './explorer_utils'; +/** + * Ignore insignificant resize, e.g. browser scrollbar appearance. + */ +const RESIZE_IGNORED_DIFF_PX = 20; const RESIZE_THROTTLE_TIME_MS = 500; +export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData { + return arg && arg.hasOwnProperty('cardinality'); +} + +/** + * Anomaly swim lane container responsible for handling resizing, pagination and injecting + * tooltip service. + * + * @param children + * @param onResize + * @param perPage + * @param fromPage + * @param swimlaneLimit + * @param onPaginationChange + * @param props + * @constructor + */ export const SwimlaneContainer: FC< Omit & { onResize: (width: number) => void; + fromPage?: number; + perPage?: number; + swimlaneLimit?: number; + onPaginationChange?: (arg: { perPage?: number; fromPage?: number }) => void; + isLoading: boolean; + noDataWarning: string | JSX.Element | null; } -> = ({ children, onResize, ...props }) => { +> = ({ + children, + onResize, + perPage, + fromPage, + swimlaneLimit, + onPaginationChange, + isLoading, + noDataWarning, + ...props +}) => { const [chartWidth, setChartWidth] = useState(0); const resizeHandler = useCallback( throttle((e: { width: number; height: number }) => { const labelWidth = 200; - setChartWidth(e.width - labelWidth); - onResize(e.width); + const resultNewWidth = e.width - labelWidth; + if (Math.abs(resultNewWidth - chartWidth) > RESIZE_IGNORED_DIFF_PX) { + setChartWidth(resultNewWidth); + onResize(resultNewWidth); + } }, RESIZE_THROTTLE_TIME_MS), - [] + [chartWidth] ); + const showSwimlane = + props.swimlaneData && + props.swimlaneData.laneLabels && + props.swimlaneData.laneLabels.length > 0 && + props.swimlaneData.points.length > 0; + + const isPaginationVisible = + (showSwimlane || isLoading) && + swimlaneLimit !== undefined && + onPaginationChange && + props.swimlaneType === SWIMLANE_TYPE.VIEW_BY && + fromPage && + perPage; + return ( - - {(resizeRef) => ( -
{ - resizeRef(el); - }} - > -
- - - {(tooltipService) => ( - + + {(resizeRef) => ( + { + resizeRef(el); + }} + data-test-subj="mlSwimLaneContainer" + > + + + {showSwimlane && !isLoading && ( + + {(tooltipService) => ( + + )} + + )} + {isLoading && ( + + + + )} + {!isLoading && !showSwimlane && ( + {noDataWarning}} /> )} - - -
-
- )} -
+ +
+ {isPaginationVisible && ( + + + + )} + + )} + + ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx new file mode 100644 index 000000000000..0607f7fd35fa --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useCallback, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiContextMenuPanel, + EuiPagination, + EuiContextMenuItem, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +interface SwimLanePaginationProps { + fromPage: number; + perPage: number; + cardinality: number; + onPaginationChange: (arg: { perPage?: number; fromPage?: number }) => void; +} + +export const SwimLanePagination: FC = ({ + cardinality, + fromPage, + perPage, + onPaginationChange, +}) => { + const componentFromPage = fromPage - 1; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = () => setIsPopoverOpen(() => !isPopoverOpen); + const closePopover = () => setIsPopoverOpen(false); + + const goToPage = useCallback((pageNumber: number) => { + onPaginationChange({ fromPage: pageNumber + 1 }); + }, []); + + const setPerPage = useCallback((perPageUpdate: number) => { + onPaginationChange({ perPage: perPageUpdate }); + }, []); + + const pageCount = Math.ceil(cardinality / perPage); + + const items = [5, 10, 20, 50, 100].map((v) => { + return ( + { + closePopover(); + setPerPage(v); + }} + > + + + ); + }); + + return ( + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 2e355c6073ab..52b4408d1ac5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -22,7 +22,6 @@ import { ml } from '../../services/ml_api_service'; import { useExplorerData } from '../../explorer/actions'; import { explorerService } from '../../explorer/explorer_dashboard_service'; import { getDateFormatTz } from '../../explorer/explorer_utils'; -import { useSwimlaneLimit } from '../../explorer/select_limit'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; import { useShowCharts } from '../../components/controls/checkbox_showcharts'; import { useTableInterval } from '../../components/controls/select_interval'; @@ -30,6 +29,7 @@ import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; +import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; const breadcrumbs = [ ML_BREADCRUMB, @@ -151,10 +151,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [showCharts] = useShowCharts(); const [tableInterval] = useTableInterval(); const [tableSeverity] = useTableSeverity(); - const [swimlaneLimit] = useSwimlaneLimit(); - useEffect(() => { - explorerService.setSwimlaneLimit(swimlaneLimit); - }, [swimlaneLimit]); const [selectedCells, setSelectedCells] = useSelectedCells(); useEffect(() => { @@ -170,14 +166,26 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim selectedCells, selectedJobs: explorerState.selectedJobs, swimlaneBucketInterval: explorerState.swimlaneBucketInterval, - swimlaneLimit: explorerState.swimlaneLimit, tableInterval: tableInterval.val, tableSeverity: tableSeverity.val, viewBySwimlaneFieldName: explorerState.viewBySwimlaneFieldName, + swimlaneContainerWidth: explorerState.swimlaneContainerWidth, + viewByPerPage: explorerState.viewByPerPage, + viewByFromPage: explorerState.viewByFromPage, }) || undefined; + useEffect(() => { - loadExplorerData(loadExplorerDataConfig); + if (explorerState && explorerState.swimlaneContainerWidth > 0) { + loadExplorerData({ + ...loadExplorerDataConfig, + swimlaneLimit: + explorerState?.viewBySwimlaneData && + isViewBySwimLaneData(explorerState?.viewBySwimlaneData) + ? explorerState?.viewBySwimlaneData.cardinality + : undefined, + }); + } }, [JSON.stringify(loadExplorerDataConfig)]); if (explorerState === undefined || refresh === undefined || showCharts === undefined) { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index ac4882b0055a..11ec074bac1d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -12,41 +12,47 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer'; -jest.mock('../../contexts/kibana/kibana_context', () => ({ - useMlKibana: () => { - return { - services: { - uiSettings: { get: jest.fn() }, - data: { - query: { - timefilter: { +jest.mock('../../contexts/kibana/kibana_context', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { of } = require('rxjs'); + return { + useMlKibana: () => { + return { + services: { + uiSettings: { get: jest.fn() }, + data: { + query: { timefilter: { - disableTimeRangeSelector: jest.fn(), - disableAutoRefreshSelector: jest.fn(), - enableTimeRangeSelector: jest.fn(), - enableAutoRefreshSelector: jest.fn(), - getRefreshInterval: jest.fn(), - setRefreshInterval: jest.fn(), - getTime: jest.fn(), - isAutoRefreshSelectorEnabled: jest.fn(), - isTimeRangeSelectorEnabled: jest.fn(), - getRefreshIntervalUpdate$: jest.fn(), - getTimeUpdate$: jest.fn(), - getEnabledUpdated$: jest.fn(), + timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), + enableTimeRangeSelector: jest.fn(), + enableAutoRefreshSelector: jest.fn(), + getRefreshInterval: jest.fn(), + setRefreshInterval: jest.fn(), + getTime: jest.fn(), + isAutoRefreshSelectorEnabled: jest.fn(), + isTimeRangeSelectorEnabled: jest.fn(), + getRefreshIntervalUpdate$: jest.fn(), + getTimeUpdate$: jest.fn(() => { + return of(); + }), + getEnabledUpdated$: jest.fn(), + }, + history: { get: jest.fn() }, }, - history: { get: jest.fn() }, }, }, - }, - notifications: { - toasts: { - addDanger: () => {}, + notifications: { + toasts: { + addDanger: () => {}, + }, }, }, - }, - }; - }, -})); + }; + }, + }; +}); jest.mock('../../util/dependency_cache', () => ({ getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), diff --git a/x-pack/plugins/ml/public/application/routing/use_refresh.ts b/x-pack/plugins/ml/public/application/routing/use_refresh.ts index f0b93c876526..c247fd9765e9 100644 --- a/x-pack/plugins/ml/public/application/routing/use_refresh.ts +++ b/x-pack/plugins/ml/public/application/routing/use_refresh.ts @@ -5,26 +5,40 @@ */ import { useObservable } from 'react-use'; -import { merge, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { merge } from 'rxjs'; +import { map, skip } from 'rxjs/operators'; +import { useMemo } from 'react'; import { annotationsRefresh$ } from '../services/annotations_service'; -import { - mlTimefilterRefresh$, - mlTimefilterTimeChange$, -} from '../services/timefilter_refresh_service'; +import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service'; +import { useTimefilter } from '../contexts/kibana'; export interface Refresh { lastRefresh: number; timeRange?: { start: string; end: string }; } -const refresh$: Observable = merge( - mlTimefilterRefresh$, - mlTimefilterTimeChange$, - annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d }))) -); - +/** + * Hook that provides the latest refresh timestamp + * and the most recent applied time range. + */ export const useRefresh = () => { + const timefilter = useTimefilter(); + + const refresh$ = useMemo(() => { + return merge( + mlTimefilterRefresh$, + timefilter.getTimeUpdate$().pipe( + // skip initially emitted value + skip(1), + map((_) => { + const { from, to } = timefilter.getTime(); + return { lastRefresh: Date.now(), timeRange: { start: from, end: to } }; + }) + ), + annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d }))) + ); + }, []); + return useObservable(refresh$); }; diff --git a/x-pack/plugins/ml/public/application/services/explorer_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts similarity index 82% rename from x-pack/plugins/ml/public/application/services/explorer_service.ts rename to x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts index 0944328db005..f2e362f754f2 100644 --- a/x-pack/plugins/ml/public/application/services/explorer_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts @@ -12,14 +12,19 @@ import { UI_SETTINGS, } from '../../../../../../src/plugins/data/public'; import { getBoundsRoundedToInterval, TimeBuckets, TimeRangeBounds } from '../util/time_buckets'; -import { ExplorerJob, OverallSwimlaneData, SwimlaneData } from '../explorer/explorer_utils'; +import { + ExplorerJob, + OverallSwimlaneData, + SwimlaneData, + ViewBySwimLaneData, +} from '../explorer/explorer_utils'; import { VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants'; import { MlResultsService } from './results_service'; /** - * Anomaly Explorer Service + * Service for retrieving anomaly swim lanes data. */ -export class ExplorerService { +export class AnomalyTimelineService { private timeBuckets: TimeBuckets; private _customTimeRange: TimeRange | undefined; @@ -130,12 +135,27 @@ export class ExplorerService { return overallSwimlaneData; } + /** + * Fetches view by swim lane data. + * + * @param fieldValues + * @param bounds + * @param selectedJobs + * @param viewBySwimlaneFieldName + * @param swimlaneLimit + * @param perPage + * @param fromPage + * @param swimlaneContainerWidth + * @param influencersFilterQuery + */ public async loadViewBySwimlane( fieldValues: string[], bounds: { earliest: number; latest: number }, selectedJobs: ExplorerJob[], viewBySwimlaneFieldName: string, swimlaneLimit: number, + perPage: number, + fromPage: number, swimlaneContainerWidth: number, influencersFilterQuery?: any ): Promise { @@ -172,7 +192,8 @@ export class ExplorerService { searchBounds.min.valueOf(), searchBounds.max.valueOf(), interval, - swimlaneLimit + perPage, + fromPage ); } else { response = await this.mlResultsService.getInfluencerValueMaxScoreByTime( @@ -183,6 +204,8 @@ export class ExplorerService { searchBounds.max.valueOf(), interval, swimlaneLimit, + perPage, + fromPage, influencersFilterQuery ); } @@ -193,6 +216,7 @@ export class ExplorerService { const viewBySwimlaneData = this.processViewByResults( response.results, + response.cardinality, fieldValues, bounds, viewBySwimlaneFieldName, @@ -204,6 +228,55 @@ export class ExplorerService { return viewBySwimlaneData; } + public async loadViewByTopFieldValuesForSelectedTime( + earliestMs: number, + latestMs: number, + selectedJobs: ExplorerJob[], + viewBySwimlaneFieldName: string, + swimlaneLimit: number, + perPage: number, + fromPage: number, + swimlaneContainerWidth: number + ) { + const selectedJobIds = selectedJobs.map((d) => d.id); + + // Find the top field values for the selected time, and then load the 'view by' + // swimlane over the full time range for those specific field values. + if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) { + const resp = await this.mlResultsService.getTopInfluencers( + selectedJobIds, + earliestMs, + latestMs, + swimlaneLimit, + perPage, + fromPage + ); + if (resp.influencers[viewBySwimlaneFieldName] === undefined) { + return []; + } + + const topFieldValues: any[] = []; + const topInfluencers = resp.influencers[viewBySwimlaneFieldName]; + if (Array.isArray(topInfluencers)) { + topInfluencers.forEach((influencerData) => { + if (influencerData.maxAnomalyScore > 0) { + topFieldValues.push(influencerData.influencerFieldValue); + } + }); + } + return topFieldValues; + } else { + const resp = await this.mlResultsService.getScoresByBucket( + selectedJobIds, + earliestMs, + latestMs, + this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asSeconds() + 's', + swimlaneLimit + ); + return Object.keys(resp.results); + } + } + private getTimeBounds(): TimeRangeBounds { return this._customTimeRange !== undefined ? this.timeFilter.calculateBounds(this._customTimeRange) @@ -245,6 +318,7 @@ export class ExplorerService { private processViewByResults( scoresByInfluencerAndTime: Record, + cardinality: number, sortedLaneValues: string[], bounds: any, viewBySwimlaneFieldName: string, @@ -254,7 +328,7 @@ export class ExplorerService { // Sorts the lanes according to the supplied array of lane // values in the order in which they should be displayed, // or pass an empty array to sort lanes according to max score over all time. - const dataset: OverallSwimlaneData = { + const dataset: ViewBySwimLaneData = { fieldName: viewBySwimlaneFieldName, points: [], laneLabels: [], @@ -262,6 +336,7 @@ export class ExplorerService { // Set the earliest and latest to be the same as the overall swim lane. earliest: bounds.earliest, latest: bounds.latest, + cardinality, }; const maxScoreByLaneLabel: Record = {}; diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts index a618534d7ae0..00adb2d32583 100644 --- a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts +++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts @@ -37,7 +37,7 @@ describe('DashboardService', () => { // assert expect(mockSavedObjectClient.find).toHaveBeenCalledWith({ type: 'dashboard', - perPage: 10, + perPage: 1000, search: `test*`, searchFields: ['title^3', 'description'], }); diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.ts index 7f2bb71d18eb..d6ccfc2f203e 100644 --- a/x-pack/plugins/ml/public/application/services/dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/services/dashboard_service.ts @@ -34,7 +34,7 @@ export function dashboardServiceProvider( async fetchDashboards(query?: string) { return await savedObjectClient.find({ type: 'dashboard', - perPage: 10, + perPage: 1000, search: query ? `${query}*` : '', searchFields: ['title^3', 'description'], }); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index af6944d7ae2d..d1b6f95f32be 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -12,7 +12,7 @@ import { annotations } from './annotations'; import { dataFrameAnalytics } from './data_frame_analytics'; import { filters } from './filters'; import { resultsApiProvider } from './results'; -import { jobs } from './jobs'; +import { jobsApiProvider } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info'; @@ -726,7 +726,7 @@ export function mlApiServicesProvider(httpService: HttpService) { dataFrameAnalytics, filters, results: resultsApiProvider(httpService), - jobs, + jobs: jobsApiProvider(httpService), fileDatavisualizer, }; } diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 6aa62da3f076..d356fc0ef339 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { http } from '../http_service'; +import { HttpService } from '../http_service'; import { basePath } from './index'; import { Dictionary } from '../../../../common/types/common'; @@ -24,10 +24,10 @@ import { import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job'; import { Category } from '../../../../common/types/categories'; -export const jobs = { +export const jobsApiProvider = (httpService: HttpService) => ({ jobsSummary(jobIds: string[]) { const body = JSON.stringify({ jobIds }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/jobs_summary`, method: 'POST', body, @@ -36,7 +36,10 @@ export const jobs = { jobsWithTimerange(dateFormatTz: string) { const body = JSON.stringify({ dateFormatTz }); - return http<{ jobs: MlJobWithTimeRange[]; jobsMap: Dictionary }>({ + return httpService.http<{ + jobs: MlJobWithTimeRange[]; + jobsMap: Dictionary; + }>({ path: `${basePath()}/jobs/jobs_with_time_range`, method: 'POST', body, @@ -45,7 +48,7 @@ export const jobs = { jobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/jobs`, method: 'POST', body, @@ -53,7 +56,7 @@ export const jobs = { }, groups() { - return http({ + return httpService.http({ path: `${basePath()}/jobs/groups`, method: 'GET', }); @@ -61,7 +64,7 @@ export const jobs = { updateGroups(updatedJobs: string[]) { const body = JSON.stringify({ jobs: updatedJobs }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/update_groups`, method: 'POST', body, @@ -75,7 +78,7 @@ export const jobs = { end, }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/force_start_datafeeds`, method: 'POST', body, @@ -84,7 +87,7 @@ export const jobs = { stopDatafeeds(datafeedIds: string[]) { const body = JSON.stringify({ datafeedIds }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/stop_datafeeds`, method: 'POST', body, @@ -93,7 +96,7 @@ export const jobs = { deleteJobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/delete_jobs`, method: 'POST', body, @@ -102,7 +105,7 @@ export const jobs = { closeJobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/close_jobs`, method: 'POST', body, @@ -111,7 +114,7 @@ export const jobs = { forceStopAndCloseJob(jobId: string) { const body = JSON.stringify({ jobId }); - return http<{ success: boolean }>({ + return httpService.http<{ success: boolean }>({ path: `${basePath()}/jobs/force_stop_and_close_job`, method: 'POST', body, @@ -121,7 +124,7 @@ export const jobs = { jobAuditMessages(jobId: string, from?: number) { const jobIdString = jobId !== undefined ? `/${jobId}` : ''; const query = from !== undefined ? { from } : {}; - return http({ + return httpService.http({ path: `${basePath()}/job_audit_messages/messages${jobIdString}`, method: 'GET', query, @@ -129,7 +132,7 @@ export const jobs = { }, deletingJobTasks() { - return http({ + return httpService.http({ path: `${basePath()}/jobs/deleting_jobs_tasks`, method: 'GET', }); @@ -137,7 +140,7 @@ export const jobs = { jobsExist(jobIds: string[]) { const body = JSON.stringify({ jobIds }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/jobs_exist`, method: 'POST', body, @@ -146,7 +149,7 @@ export const jobs = { newJobCaps(indexPatternTitle: string, isRollup: boolean = false) { const query = isRollup === true ? { rollup: true } : {}; - return http({ + return httpService.http({ path: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}`, method: 'GET', query, @@ -175,7 +178,7 @@ export const jobs = { splitFieldName, splitFieldValue, }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/new_job_line_chart`, method: 'POST', body, @@ -202,7 +205,7 @@ export const jobs = { aggFieldNamePairs, splitFieldName, }); - return http({ + return httpService.http({ path: `${basePath()}/jobs/new_job_population_chart`, method: 'POST', body, @@ -210,7 +213,7 @@ export const jobs = { }, getAllJobAndGroupIds() { - return http({ + return httpService.http({ path: `${basePath()}/jobs/all_jobs_and_group_ids`, method: 'GET', }); @@ -222,7 +225,7 @@ export const jobs = { start, end, }); - return http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({ + return httpService.http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({ path: `${basePath()}/jobs/look_back_progress`, method: 'POST', body, @@ -249,7 +252,7 @@ export const jobs = { end, analyzer, }); - return http<{ + return httpService.http<{ examples: CategoryFieldExample[]; sampleSize: number; overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS; @@ -263,7 +266,10 @@ export const jobs = { topCategories(jobId: string, count: number) { const body = JSON.stringify({ jobId, count }); - return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({ + return httpService.http<{ + total: number; + categories: Array<{ count?: number; category: Category }>; + }>({ path: `${basePath()}/jobs/top_categories`, method: 'POST', body, @@ -278,10 +284,13 @@ export const jobs = { calendarEvents?: Array<{ start: number; end: number; description: string }> ) { const body = JSON.stringify({ jobId, snapshotId, replay, end, calendarEvents }); - return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({ + return httpService.http<{ + total: number; + categories: Array<{ count?: number; category: Category }>; + }>({ path: `${basePath()}/jobs/revert_model_snapshot`, method: 'POST', body, }); }, -}; +}); diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index 1b2c01ab73fc..b26528b76037 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -14,9 +14,19 @@ export function resultsServiceProvider( earliestMs: number, latestMs: number, interval: string | number, - maxResults: number + perPage?: number, + fromPage?: number + ): Promise; + getTopInfluencers( + selectedJobIds: string[], + earliestMs: number, + latestMs: number, + maxFieldValues: number, + perPage?: number, + fromPage?: number, + influencers?: any[], + influencersFilterQuery?: any ): Promise; - getTopInfluencers(): Promise; getTopInfluencerValues(): Promise; getOverallBucketScores( jobIds: any, @@ -33,6 +43,8 @@ export function resultsServiceProvider( latestMs: number, interval: string, maxResults: number, + perPage: number, + fromPage: number, influencersFilterQuery: any ): Promise; getRecordInfluencers(): Promise; diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index 9e3fed189b6f..55ddb1de3529 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -9,6 +9,10 @@ import _ from 'lodash'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { escapeForElasticsearchQuery } from '../../util/string_utils'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; +import { + ANOMALY_SWIM_LANE_HARD_LIMIT, + SWIM_LANE_DEFAULT_PAGE_SIZE, +} from '../../explorer/explorer_constants'; /** * Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards. @@ -24,7 +28,7 @@ export function resultsServiceProvider(mlApiServices) { // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a results property, with a key for job // which has results for the specified time range. - getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { + getScoresByBucket(jobIds, earliestMs, latestMs, interval, perPage = 10, fromPage = 1) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -88,7 +92,7 @@ export function resultsServiceProvider(mlApiServices) { jobId: { terms: { field: 'job_id', - size: maxResults !== undefined ? maxResults : 5, + size: jobIds?.length ?? 1, order: { anomalyScore: 'desc', }, @@ -99,6 +103,12 @@ export function resultsServiceProvider(mlApiServices) { field: 'anomaly_score', }, }, + bucketTruncate: { + bucket_sort: { + from: (fromPage - 1) * perPage, + size: perPage === 0 ? 1 : perPage, + }, + }, byTime: { date_histogram: { field: 'timestamp', @@ -158,7 +168,9 @@ export function resultsServiceProvider(mlApiServices) { jobIds, earliestMs, latestMs, - maxFieldValues = 10, + maxFieldValues = ANOMALY_SWIM_LANE_HARD_LIMIT, + perPage = 10, + fromPage = 1, influencers = [], influencersFilterQuery ) { @@ -272,6 +284,12 @@ export function resultsServiceProvider(mlApiServices) { }, }, aggs: { + bucketTruncate: { + bucket_sort: { + from: (fromPage - 1) * perPage, + size: perPage, + }, + }, maxAnomalyScore: { max: { field: 'influencer_score', @@ -472,7 +490,9 @@ export function resultsServiceProvider(mlApiServices) { earliestMs, latestMs, interval, - maxResults, + maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT, + perPage = SWIM_LANE_DEFAULT_PAGE_SIZE, + fromPage = 1, influencersFilterQuery ) { return new Promise((resolve, reject) => { @@ -565,10 +585,15 @@ export function resultsServiceProvider(mlApiServices) { }, }, aggs: { + influencerValuesCardinality: { + cardinality: { + field: 'influencer_field_value', + }, + }, influencerFieldValues: { terms: { field: 'influencer_field_value', - size: maxResults !== undefined ? maxResults : 10, + size: !!maxResults ? maxResults : ANOMALY_SWIM_LANE_HARD_LIMIT, order: { maxAnomalyScore: 'desc', }, @@ -579,6 +604,12 @@ export function resultsServiceProvider(mlApiServices) { field: 'influencer_score', }, }, + bucketTruncate: { + bucket_sort: { + from: (fromPage - 1) * perPage, + size: perPage, + }, + }, byTime: { date_histogram: { field: 'timestamp', @@ -618,6 +649,8 @@ export function resultsServiceProvider(mlApiServices) { obj.results[fieldValue] = fieldValues; }); + obj.cardinality = resp.aggregations?.influencerValuesCardinality?.value ?? 0; + resolve(obj); }) .catch((resp) => { diff --git a/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx b/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx index 86c07a3577f7..4f5d0723d65a 100644 --- a/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx +++ b/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx @@ -9,4 +9,3 @@ import { Subject } from 'rxjs'; import { Refresh } from '../routing/use_refresh'; export const mlTimefilterRefresh$ = new Subject>(); -export const mlTimefilterTimeChange$ = new Subject>(); 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 450c166f9030..7411820ba323 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/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 3b4562628051..83070a5d94ba 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -16,10 +16,10 @@ import { IContainer, } from '../../../../../../src/plugins/embeddable/public'; import { MlStartDependencies } from '../../plugin'; -import { ExplorerSwimlaneContainer } from './explorer_swimlane_container'; +import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import { JobId } from '../../../common/types/anomaly_detection_jobs'; -import { ExplorerService } from '../../application/services/explorer_service'; +import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service'; import { Filter, Query, @@ -40,7 +40,7 @@ export interface AnomalySwimlaneEmbeddableCustomInput { jobIds: JobId[]; swimlaneType: SwimlaneType; viewBy?: string; - limit?: number; + perPage?: number; // Embeddable inputs which are not included in the default interface filters: Filter[]; @@ -58,12 +58,12 @@ export interface AnomalySwimlaneEmbeddableCustomOutput { jobIds: JobId[]; swimlaneType: SwimlaneType; viewBy?: string; - limit?: number; + perPage?: number; } export interface AnomalySwimlaneServices { anomalyDetectorService: AnomalyDetectorService; - explorerService: ExplorerService; + anomalyTimelineService: AnomalyTimelineService; } export type AnomalySwimlaneEmbeddableServices = [ @@ -101,14 +101,20 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< super.render(node); this.node = node; + const I18nContext = this.services[0].i18n.Context; + ReactDOM.render( - this.updateOutput(output)} - />, + + { + this.updateInput(input); + }} + /> + , node ); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx index 6b2ab89de8a5..243369982ac1 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx @@ -46,6 +46,9 @@ describe('AnomalySwimlaneEmbeddableFactory', () => { }); expect(Object.keys(createServices[0])).toEqual(Object.keys(coreStart)); expect(createServices[1]).toMatchObject(pluginsStart); - expect(Object.keys(createServices[2])).toEqual(['anomalyDetectorService', 'explorerService']); + expect(Object.keys(createServices[2])).toEqual([ + 'anomalyDetectorService', + 'anomalyTimelineService', + ]); }); }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index 37c2cfb3e029..0d587b428d89 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -22,7 +22,7 @@ import { import { MlStartDependencies } from '../../plugin'; import { HttpService } from '../../application/services/http_service'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; -import { ExplorerService } from '../../application/services/explorer_service'; +import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service'; import { mlResultsServiceProvider } from '../../application/services/results_service'; import { resolveAnomalySwimlaneUserInput } from './anomaly_swimlane_setup_flyout'; import { mlApiServicesProvider } from '../../application/services/ml_api_service'; @@ -44,14 +44,10 @@ export class AnomalySwimlaneEmbeddableFactory } public async getExplicitInput(): Promise> { - const [{ overlays, uiSettings }, , { anomalyDetectorService }] = await this.getServices(); + const [coreStart] = await this.getServices(); try { - return await resolveAnomalySwimlaneUserInput({ - anomalyDetectorService, - overlays, - uiSettings, - }); + return await resolveAnomalySwimlaneUserInput(coreStart); } catch (e) { return Promise.reject(); } @@ -62,13 +58,13 @@ export class AnomalySwimlaneEmbeddableFactory const httpService = new HttpService(coreStart.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); - const explorerService = new ExplorerService( + const anomalyTimelineService = new AnomalyTimelineService( pluginsStart.data.query.timefilter.timefilter, coreStart.uiSettings, mlResultsServiceProvider(mlApiServicesProvider(httpService)) ); - return [coreStart, pluginsStart, { anomalyDetectorService, explorerService }]; + return [coreStart, pluginsStart, { anomalyDetectorService, anomalyTimelineService }]; } public async create( diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx index 4977ece54bb5..be9a332e51db 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx @@ -27,7 +27,7 @@ export interface AnomalySwimlaneInitializerProps { defaultTitle: string; influencers: string[]; initialInput?: Partial< - Pick + Pick >; onCreate: (swimlaneProps: { panelTitle: string; @@ -38,11 +38,6 @@ export interface AnomalySwimlaneInitializerProps { onCancel: () => void; } -const limitOptions = [5, 10, 25, 50].map((limit) => ({ - value: limit, - text: `${limit}`, -})); - export const AnomalySwimlaneInitializer: FC = ({ defaultTitle, influencers, @@ -55,7 +50,6 @@ export const AnomalySwimlaneInitializer: FC = ( initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL ); const [viewBySwimlaneFieldName, setViewBySwimlaneFieldName] = useState(initialInput?.viewBy); - const [limit, setLimit] = useState(initialInput?.limit ?? 5); const swimlaneTypeOptions = [ { @@ -154,19 +148,6 @@ export const AnomalySwimlaneInitializer: FC = ( onChange={(e) => setViewBySwimlaneFieldName(e.target.value)} /> - - } - > - setLimit(Number(e.target.value))} - /> - )} @@ -186,7 +167,6 @@ export const AnomalySwimlaneInitializer: FC = ( panelTitle, swimlaneType, viewBy: viewBySwimlaneFieldName, - limit, })} fill > diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index 54f50d2d3da3..1ffdadb60aaa 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -5,10 +5,13 @@ */ import React from 'react'; -import { IUiSettingsClient, OverlayStart } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import moment from 'moment'; import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + toMountPoint, +} from '../../../../../../src/plugins/kibana_react/public'; import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer'; import { JobSelectorFlyout } from '../../application/components/job_selector/job_selector_flyout'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; @@ -17,19 +20,17 @@ import { AnomalySwimlaneEmbeddableInput, getDefaultPanelTitle, } from './anomaly_swimlane_embeddable'; +import { getMlGlobalServices } from '../../application/app'; +import { HttpService } from '../../application/services/http_service'; export async function resolveAnomalySwimlaneUserInput( - { - overlays, - anomalyDetectorService, - uiSettings, - }: { - anomalyDetectorService: AnomalyDetectorService; - overlays: OverlayStart; - uiSettings: IUiSettingsClient; - }, + coreStart: CoreStart, input?: AnomalySwimlaneEmbeddableInput ): Promise> { + const { http, uiSettings, overlays } = coreStart; + + const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http)); + return new Promise(async (resolve, reject) => { const maps = { groupsMap: getInitialGroupsMap([]), @@ -41,48 +42,50 @@ export async function resolveAnomalySwimlaneUserInput( const selectedIds = input?.jobIds; - const flyoutSession = overlays.openFlyout( + const flyoutSession = coreStart.overlays.openFlyout( toMountPoint( - { - flyoutSession.close(); - reject(); - }} - onSelectionConfirmed={async ({ jobIds, groups }) => { - const title = input?.title ?? getDefaultPanelTitle(jobIds); + + { + flyoutSession.close(); + reject(); + }} + onSelectionConfirmed={async ({ jobIds, groups }) => { + const title = input?.title ?? getDefaultPanelTitle(jobIds); - const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); + const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); - const influencers = anomalyDetectorService.extractInfluencers(jobs); - influencers.push(VIEW_BY_JOB_LABEL); + const influencers = anomalyDetectorService.extractInfluencers(jobs); + influencers.push(VIEW_BY_JOB_LABEL); - await flyoutSession.close(); + await flyoutSession.close(); - const modalSession = overlays.openModal( - toMountPoint( - { - modalSession.close(); - resolve({ jobIds, title: panelTitle, swimlaneType, viewBy, limit }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> - ) - ); - }} - maps={maps} - /> + const modalSession = overlays.openModal( + toMountPoint( + { + modalSession.close(); + resolve({ jobIds, title: panelTitle, swimlaneType, viewBy }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + ) + ); + }} + maps={maps} + /> + ), { 'data-test-subj': 'mlAnomalySwimlaneEmbeddable', diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx similarity index 73% rename from x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx rename to x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx index 63ae89b5acdd..846a3f543c2d 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { ExplorerSwimlaneContainer } from './explorer_swimlane_container'; +import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container'; import { BehaviorSubject, Observable } from 'rxjs'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -17,6 +17,7 @@ import { CoreStart } from 'kibana/public'; import { MlStartDependencies } from '../../plugin'; import { useSwimlaneInputResolver } from './swimlane_input_resolver'; import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; +import { SwimlaneContainer } from '../../application/explorer/swimlane_container'; jest.mock('./swimlane_input_resolver', () => ({ useSwimlaneInputResolver: jest.fn(() => { @@ -24,12 +25,11 @@ jest.mock('./swimlane_input_resolver', () => ({ }), })); -jest.mock('../../application/explorer/explorer_swimlane', () => ({ - ExplorerSwimlane: jest.fn(), -})); - -jest.mock('../../application/components/chart_tooltip', () => ({ - MlTooltipComponent: jest.fn(), +jest.mock('../../application/explorer/swimlane_container', () => ({ + SwimlaneContainer: jest.fn(() => { + return null; + }), + isViewBySwimLaneData: jest.fn(), })); const defaultOptions = { wrapper: I18nProvider }; @@ -38,6 +38,7 @@ describe('ExplorerSwimlaneContainer', () => { let embeddableInput: BehaviorSubject>; let refresh: BehaviorSubject; let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices]; + const onInputChange = jest.fn(); beforeEach(() => { embeddableInput = new BehaviorSubject({ @@ -61,25 +62,39 @@ describe('ExplorerSwimlaneContainer', () => { }; (useSwimlaneInputResolver as jest.Mock).mockReturnValueOnce([ - mockOverallData, SWIMLANE_TYPE.OVERALL, - undefined, + mockOverallData, + 10, + jest.fn(), + {}, + false, + null, ]); - const { findByTestId } = render( - } services={services} refresh={refresh} + onInputChange={onInputChange} />, defaultOptions ); - expect( - await findByTestId('mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable') - ).toBeDefined(); + + const calledWith = ((SwimlaneContainer as unknown) as jest.Mock).mock + .calls[0][0]; + + expect(calledWith).toMatchObject({ + perPage: 10, + swimlaneType: SWIMLANE_TYPE.OVERALL, + swimlaneData: mockOverallData, + isLoading: false, + swimlaneLimit: undefined, + fromPage: 1, + }); }); test('should render an error in case it could not fetch the ML swimlane data', async () => { @@ -87,38 +102,25 @@ describe('ExplorerSwimlaneContainer', () => { undefined, undefined, undefined, + undefined, + undefined, + false, { message: 'Something went wrong' }, ]); const { findByText } = render( - } services={services} refresh={refresh} + onInputChange={onInputChange} />, defaultOptions ); const errorMessage = await findByText('Something went wrong'); expect(errorMessage).toBeDefined(); }); - - test('should render a loading indicator during the data fetching', async () => { - const { findByTestId } = render( - - } - services={services} - refresh={refresh} - />, - defaultOptions - ); - expect( - await findByTestId('loading_mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable') - ).toBeDefined(); - }); }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx new file mode 100644 index 000000000000..5d91bdb41df6 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { Observable } from 'rxjs'; + +import { CoreStart } from 'kibana/public'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MlStartDependencies } from '../../plugin'; +import { + AnomalySwimlaneEmbeddableInput, + AnomalySwimlaneEmbeddableOutput, + AnomalySwimlaneServices, +} from './anomaly_swimlane_embeddable'; +import { useSwimlaneInputResolver } from './swimlane_input_resolver'; +import { SwimlaneType } from '../../application/explorer/explorer_constants'; +import { + isViewBySwimLaneData, + SwimlaneContainer, +} from '../../application/explorer/swimlane_container'; + +export interface ExplorerSwimlaneContainerProps { + id: string; + embeddableInput: Observable; + services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices]; + refresh: Observable; + onInputChange: (output: Partial) => void; +} + +export const EmbeddableSwimLaneContainer: FC = ({ + id, + embeddableInput, + services, + refresh, + onInputChange, +}) => { + const [chartWidth, setChartWidth] = useState(0); + const [fromPage, setFromPage] = useState(1); + + const [ + swimlaneType, + swimlaneData, + perPage, + setPerPage, + timeBuckets, + isLoading, + error, + ] = useSwimlaneInputResolver( + embeddableInput, + onInputChange, + refresh, + services, + chartWidth, + fromPage + ); + + if (error) { + return ( + + } + color="danger" + iconType="alert" + style={{ width: '100%' }} + > +

{error.message}

+
+ ); + } + + return ( +
+ { + setChartWidth(width); + }} + onPaginationChange={(update) => { + if (update.fromPage) { + setFromPage(update.fromPage); + } + if (update.perPage) { + setFromPage(1); + setPerPage(update.perPage); + } + }} + isLoading={isLoading} + noDataWarning={ + + } + /> +
+ ); +}; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx deleted file mode 100644 index db2b9d55cfab..000000000000 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx +++ /dev/null @@ -1,126 +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, { FC, useCallback, useState } from 'react'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingChart, - EuiResizeObserver, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { Observable } from 'rxjs'; - -import { throttle } from 'lodash'; -import { CoreStart } from 'kibana/public'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ExplorerSwimlane } from '../../application/explorer/explorer_swimlane'; -import { MlStartDependencies } from '../../plugin'; -import { - AnomalySwimlaneEmbeddableInput, - AnomalySwimlaneEmbeddableOutput, - AnomalySwimlaneServices, -} from './anomaly_swimlane_embeddable'; -import { MlTooltipComponent } from '../../application/components/chart_tooltip'; -import { useSwimlaneInputResolver } from './swimlane_input_resolver'; -import { SwimlaneType } from '../../application/explorer/explorer_constants'; - -const RESIZE_THROTTLE_TIME_MS = 500; - -export interface ExplorerSwimlaneContainerProps { - id: string; - embeddableInput: Observable; - services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices]; - refresh: Observable; - onOutputChange?: (output: Partial) => void; -} - -export const ExplorerSwimlaneContainer: FC = ({ - id, - embeddableInput, - services, - refresh, -}) => { - const [chartWidth, setChartWidth] = useState(0); - - const [swimlaneType, swimlaneData, timeBuckets, error] = useSwimlaneInputResolver( - embeddableInput, - refresh, - services, - chartWidth - ); - - const onResize = useCallback( - throttle((e: { width: number; height: number }) => { - const labelWidth = 200; - setChartWidth(e.width - labelWidth); - }, RESIZE_THROTTLE_TIME_MS), - [] - ); - - if (error) { - return ( - - } - color="danger" - iconType="alert" - style={{ width: '100%' }} - > -

{error.message}

-
- ); - } - - return ( - - {(resizeRef) => ( -
{ - resizeRef(el); - }} - > -
- - - {chartWidth > 0 && swimlaneData && swimlaneType ? ( - - - {(tooltipService) => ( - - )} - - - ) : ( - - - - - - )} -
-
- )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts index 890c2bde6305..a34955adebf6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts @@ -19,6 +19,7 @@ describe('useSwimlaneInputResolver', () => { let embeddableInput: BehaviorSubject>; let refresh: Subject; let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices]; + let onInputChange: jest.Mock; beforeEach(() => { jest.useFakeTimers(); @@ -41,7 +42,7 @@ describe('useSwimlaneInputResolver', () => { } as CoreStart, (null as unknown) as MlStartDependencies, ({ - explorerService: { + anomalyTimelineService: { setTimeRange: jest.fn(), loadOverallData: jest.fn(() => Promise.resolve({ @@ -69,6 +70,7 @@ describe('useSwimlaneInputResolver', () => { }, } as unknown) as AnomalySwimlaneServices, ]; + onInputChange = jest.fn(); }); afterEach(() => { jest.useRealTimers(); @@ -79,9 +81,11 @@ describe('useSwimlaneInputResolver', () => { const { result, waitForNextUpdate } = renderHook(() => useSwimlaneInputResolver( embeddableInput as Observable, + onInputChange, refresh, services, - 1000 + 1000, + 1 ) ); @@ -94,7 +98,7 @@ describe('useSwimlaneInputResolver', () => { }); expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(1); - expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(1); + expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(1); await act(async () => { embeddableInput.next({ @@ -109,7 +113,7 @@ describe('useSwimlaneInputResolver', () => { }); expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); - expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(2); + expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(2); await act(async () => { embeddableInput.next({ @@ -124,7 +128,7 @@ describe('useSwimlaneInputResolver', () => { }); expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); - expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(3); + expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3); }); }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 3829bbce5e5c..9ed6f88150f6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -16,23 +16,31 @@ import { skipWhile, startWith, switchMap, + tap, } from 'rxjs/operators'; import { CoreStart } from 'kibana/public'; import { TimeBuckets } from '../../application/util/time_buckets'; import { AnomalySwimlaneEmbeddableInput, + AnomalySwimlaneEmbeddableOutput, AnomalySwimlaneServices, } from './anomaly_swimlane_embeddable'; import { MlStartDependencies } from '../../plugin'; -import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants'; +import { + ANOMALY_SWIM_LANE_HARD_LIMIT, + SWIM_LANE_DEFAULT_PAGE_SIZE, + SWIMLANE_TYPE, + SwimlaneType, +} from '../../application/explorer/explorer_constants'; import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters'; import { Query } from '../../../../../../src/plugins/data/common/query'; import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { ExplorerJob, OverallSwimlaneData } from '../../application/explorer/explorer_utils'; import { parseInterval } from '../../../common/util/parse_interval'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; +import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -const RESIZE_IGNORED_DIFF_PX = 20; const FETCH_RESULTS_DEBOUNCE_MS = 500; function getJobsObservable( @@ -48,17 +56,31 @@ function getJobsObservable( export function useSwimlaneInputResolver( embeddableInput: Observable, + onInputChange: (output: Partial) => void, refresh: Observable, services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices], - chartWidth: number -): [string | undefined, OverallSwimlaneData | undefined, TimeBuckets, Error | null | undefined] { - const [{ uiSettings }, , { explorerService, anomalyDetectorService }] = services; + chartWidth: number, + fromPage: number +): [ + string | undefined, + OverallSwimlaneData | undefined, + number, + (perPage: number) => void, + TimeBuckets, + boolean, + Error | null | undefined +] { + const [{ uiSettings }, , { anomalyTimelineService, anomalyDetectorService }] = services; const [swimlaneData, setSwimlaneData] = useState(); const [swimlaneType, setSwimlaneType] = useState(); const [error, setError] = useState(); + const [perPage, setPerPage] = useState(); + const [isLoading, setIsLoading] = useState(false); const chartWidth$ = useMemo(() => new Subject(), []); + const fromPage$ = useMemo(() => new Subject(), []); + const perPage$ = useMemo(() => new Subject(), []); const timeBuckets = useMemo(() => { return new TimeBuckets({ @@ -73,28 +95,32 @@ export function useSwimlaneInputResolver( const subscription = combineLatest([ getJobsObservable(embeddableInput, anomalyDetectorService), embeddableInput, - chartWidth$.pipe( - skipWhile((v) => !v), - distinctUntilChanged((prev, curr) => { - // emit only if the width has been changed significantly - return Math.abs(curr - prev) < RESIZE_IGNORED_DIFF_PX; - }) + chartWidth$.pipe(skipWhile((v) => !v)), + fromPage$, + perPage$.pipe( + startWith(undefined), + // no need to emit when the initial value has been set + distinctUntilChanged( + (prev, curr) => prev === undefined && curr === SWIM_LANE_DEFAULT_PAGE_SIZE + ) ), refresh.pipe(startWith(null)), ]) .pipe( + tap(setIsLoading.bind(null, true)), debounceTime(FETCH_RESULTS_DEBOUNCE_MS), - switchMap(([jobs, input, swimlaneContainerWidth]) => { + switchMap(([jobs, input, swimlaneContainerWidth, fromPageInput, perPageFromState]) => { const { viewBy, swimlaneType: swimlaneTypeInput, - limit, + perPage: perPageInput, timeRange, filters, query, + viewMode, } = input; - explorerService.setTimeRange(timeRange); + anomalyTimelineService.setTimeRange(timeRange); if (!swimlaneType) { setSwimlaneType(swimlaneTypeInput); @@ -118,18 +144,34 @@ export function useSwimlaneInputResolver( return of(undefined); } - return from(explorerService.loadOverallData(explorerJobs, swimlaneContainerWidth)).pipe( + return from( + anomalyTimelineService.loadOverallData(explorerJobs, swimlaneContainerWidth) + ).pipe( switchMap((overallSwimlaneData) => { const { earliest, latest } = overallSwimlaneData; if (overallSwimlaneData && swimlaneTypeInput === SWIMLANE_TYPE.VIEW_BY) { + if (perPageFromState === undefined) { + // set initial pagination from the input or default one + setPerPage(perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE); + } + + if (viewMode === ViewMode.EDIT && perPageFromState !== perPageInput) { + // store per page value when the dashboard is in the edit mode + onInputChange({ perPage: perPageFromState }); + } + return from( - explorerService.loadViewBySwimlane( + anomalyTimelineService.loadViewBySwimlane( [], { earliest, latest }, explorerJobs, viewBy!, - limit!, + isViewBySwimLaneData(swimlaneData) + ? swimlaneData.cardinality + : ANOMALY_SWIM_LANE_HARD_LIMIT, + perPageFromState ?? perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE, + fromPageInput, swimlaneContainerWidth, appliedFilters ) @@ -156,6 +198,7 @@ export function useSwimlaneInputResolver( if (data !== undefined) { setError(null); setSwimlaneData(data); + setIsLoading(false); } }); @@ -164,11 +207,28 @@ export function useSwimlaneInputResolver( }; }, []); + useEffect(() => { + fromPage$.next(fromPage); + }, [fromPage]); + + useEffect(() => { + if (perPage === undefined) return; + perPage$.next(perPage); + }, [perPage]); + useEffect(() => { chartWidth$.next(chartWidth); }, [chartWidth]); - return [swimlaneType, swimlaneData, timeBuckets, error]; + return [ + swimlaneType, + swimlaneData, + perPage ?? SWIM_LANE_DEFAULT_PAGE_SIZE, + setPerPage, + timeBuckets, + isLoading, + error, + ]; } export function processFilters(filters: Filter[], query: Query) { diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx index 312b9f31124b..0db41c1ed104 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx @@ -14,8 +14,6 @@ import { AnomalySwimlaneEmbeddableOutput, } from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; import { resolveAnomalySwimlaneUserInput } from '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout'; -import { HttpService } from '../application/services/http_service'; -import { AnomalyDetectorService } from '../application/services/anomaly_detector_service'; export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction'; @@ -39,18 +37,10 @@ export function createEditSwimlanePanelAction(getStartServices: CoreSetup['getSt throw new Error('Not possible to execute an action without the embeddable context'); } - const [{ overlays, uiSettings, http }] = await getStartServices(); - const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http)); + const [coreStart] = await getStartServices(); try { - const result = await resolveAnomalySwimlaneUserInput( - { - anomalyDetectorService, - overlays, - uiSettings, - }, - embeddable.getInput() - ); + const result = await resolveAnomalySwimlaneUserInput(coreStart, embeddable.getInput()); embeddable.updateInput(result); } catch (e) { return Promise.reject(); 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 1ed9df8da65d..ae9a56f00a5c 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_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json index 5e185e80a603..f8feaef3be5f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json @@ -1,29 +1,29 @@ { "id": "apm_transaction", "title": "APM", - "description": "Detect anomalies in high mean of transaction duration (ECS).", + "description": "Detect anomalies in transactions from your APM services.", "type": "Transaction data", "logoFile": "logo.json", - "defaultIndexPattern": "apm-*", + "defaultIndexPattern": "apm-*-transaction", "query": { "bool": { "filter": [ { "term": { "processor.event": "transaction" } }, - { "term": { "transaction.type": "request" } } + { "exists": { "field": "transaction.duration" } } ] } }, "jobs": [ { - "id": "high_mean_response_time", - "file": "high_mean_response_time.json" + "id": "high_mean_transaction_duration", + "file": "high_mean_transaction_duration.json" } ], "datafeeds": [ { - "id": "datafeed-high_mean_response_time", - "file": "datafeed_high_mean_response_time.json", - "job_id": "high_mean_response_time" + "id": "datafeed-high_mean_transaction_duration", + "file": "datafeed_high_mean_transaction_duration.json", + "job_id": "high_mean_transaction_duration" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json similarity index 75% rename from x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json index dc37d05d1811..d312577902f5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json @@ -7,7 +7,7 @@ "bool": { "filter": [ { "term": { "processor.event": "transaction" } }, - { "term": { "transaction.type": "request" } } + { "exists": { "field": "transaction.duration.us" } } ] } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json deleted file mode 100644 index f6c230a6792f..000000000000 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "job_type": "anomaly_detector", - "groups": [ - "apm" - ], - "description": "Detect anomalies in high mean of transaction duration", - "analysis_config": { - "bucket_span": "15m", - "detectors": [ - { - "detector_description": "high_mean(\"transaction.duration.us\")", - "function": "high_mean", - "field_name": "transaction.duration.us" - } - ], - "influencers": [] - }, - "analysis_limits": { - "model_memory_limit": "10mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "model_plot_config": { - "enabled": true - }, - "custom_settings": { - "created_by": "ml-module-apm-transaction" - } -} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json new file mode 100644 index 000000000000..77284cb275cd --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json @@ -0,0 +1,35 @@ +{ + "job_type": "anomaly_detector", + "groups": [ + "apm" + ], + "description": "Detect transaction duration anomalies across transaction types for your APM services.", + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "high duration by transaction type for an APM service", + "function": "high_mean", + "field_name": "transaction.duration.us", + "by_field_name": "transaction.type", + "partition_field_name": "service.name" + } + ], + "influencers": [ + "transaction.type", + "service.name" + ] + }, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "model_plot_config": { + "enabled": true + }, + "custom_settings": { + "created_by": "ml-module-apm-transaction" + } +} 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 78cc7901363c..d58c797b446d 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 ade3d3eca90e..88d24a1b86b6 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 23148c14c734..e2b58cf2ce8f 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 7b4b2a55c29f..3fca8ea1ba04 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 d1666c5c1bf7..27935fd6fe21 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 726d4be4924d..9ebb074ec7c3 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 b3fc70e9ffd7..59e838c449a3 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 b76f4eb5b75a..8232e0a8908d 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 9c5981585a8d..9acfce1e8c0b 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 0ab3683f4b72..297ce49f1f14 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 14f838cff7a3..12bd3a7575cf 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 98d56f9790be..2255022dcece 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 9467535d556b..163688d77202 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 5afb382b7cda..2a4caf17515e 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 341309004b11..caa21cd8ee8d 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 c3fbe266be6d..cc3682ef764c 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 40070a6b0d0f..19ed8298391d 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 cf5a99525cc4..9508260a6413 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 06f6cf4f1a5e..30ec728546ce 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 ef878e489255..a5d43d1da7eb 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 f630903d4e29..10a75b9d1ca8 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 c6575393590f..74d4bd6d2b5d 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 4e5e439ff90d..d1a7aec2f153 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 878287577553..39855e7f10ea 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 2a5c15ece4b4..e4a36fdf35da 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 e65d1779520c..e57dfebb3641 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 d7a708173d3a..27913fafe325 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 37441edce0af..d0800c7b24fe 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 898b123e976f..bca9496bc9ad 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 5c66bd599dd9..3c892fe6120a 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 db7137c30513..305fb6bab547 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 a4c634439ec4..acfae5138154 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 2a6d08c0740d..213bea3bc3ee 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 151eff31f8a0..8c326c3f8a78 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 d70fbb89c065..df9b63bc5fa3 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 3a61518a850e..56225639777c 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 aa95bbbd9cf0..3ebc7e5c8192 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 8791b5173b89..90f53a91e425 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 50898f94586f..549f6ab06374 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 a1edf87c33ba..6cf33334d928 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 7f58482d35b1..d75c7b585994 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 59118ef6f8ec..3dbbe70bfc56 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 f21fc2c12a00..9434747028e5 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 53a3af38f323..76be39a2c0e0 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 c679098bc05b..aa06d3f696d0 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,28 +136,31 @@ async function fetchRollupVisualizations( let rollupVisualizations = 0; let rollupVisualizationsFromSavedSearches = 0; - visualizations.forEach((visualization) => { - const { - _source: { - visualization: { - savedSearchRefName, - kibanaSavedObjectMeta: { searchSourceJSON }, - }, - references = [] as any[], - }, - } = visualization; - - const searchSource = JSON.parse(searchSourceJSON); - - if (savedSearchRefName) { + visualizations.forEach((visualization: any) => { + const references: Array<{ name: string; id: string }> | undefined = get( + visualization, + '_source.references' + ); + const savedSearchRefName: string | undefined = get( + visualization, + '_source.visualization.savedSearchRefName' + ); + const searchSourceJSON: string | undefined = get( + visualization, + '_source.visualization.kibanaSavedObjectMeta.searchSourceJSON' + ); + + if (savedSearchRefName && references?.length) { // This visualization depends upon a saved search. - const savedSearch = references.find((ref) => ref.name === savedSearchRefName); - if (rollupSavedSearchesToFlagMap[savedSearch.id]) { + const savedSearch = references.find(({ name }) => name === savedSearchRefName); + if (savedSearch && rollupSavedSearchesToFlagMap[savedSearch.id]) { rollupVisualizations++; rollupVisualizationsFromSavedSearches++; } - } else { + } else if (searchSourceJSON) { // This visualization depends upon an index pattern. + const searchSource = JSON.parse(searchSourceJSON); + if (rollupIndexPatternToFlagMap[searchSource.index]) { rollupVisualizations++; } 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 815fe163411b..885836780f1a 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 546d9d628277..250947d72c5f 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 f5daa6b38de4..e4dded600dcf 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 b023f1b365c0..0fb0522d449b 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 10aa59083dff..14375587c849 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.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 6bc829f766e5..2a0922d614f1 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -846,4 +846,43 @@ describe('FeatureTable', () => { }, }); }); + + it('does not render features which lack privileges', () => { + const role = createRole([ + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ]); + + const featureWithoutPrivileges = createFeature({ + id: 'no_privs', + name: 'No Privileges Feature', + privileges: null, + }); + + const { displayedPrivileges } = setup({ + role, + features: [...kibanaFeatures, featureWithoutPrivileges], + privilegeIndex: 0, + calculateDisplayedPrivileges: true, + canCustomizeSubFeaturePrivileges: false, + }); + + expect(displayedPrivileges).toEqual({ + excluded_from_base: { + primaryFeaturePrivilege: 'none', + }, + no_sub_features: { + primaryFeaturePrivilege: 'none', + }, + with_excluded_sub_features: { + primaryFeaturePrivilege: 'none', + }, + with_sub_features: { + primaryFeaturePrivilege: 'none', + }, + }); + }); }); 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 38e4390a2856..57e24f283822 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'; @@ -64,7 +63,9 @@ export class FeatureTable extends Component { public render() { const { role, kibanaPrivileges } = this.props; - const featurePrivileges = kibanaPrivileges.getSecuredFeatures(); + const featurePrivileges = kibanaPrivileges + .getSecuredFeatures() + .filter((feature) => feature.privileges != null || feature.reserved != null); const items: TableRow[] = featurePrivileges .sort((feature1, feature2) => { 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 7b5d8d8c1ed2..204fb512abcf 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 585c07c2e834..64b7fe3e2e3a 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 f5c85d3d92be..cc9e74805040 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 7a9779430355..296a8f6c8693 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/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index a1bedea9f7de..45f55b34baf9 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -50,7 +50,7 @@ describe('usingPrivileges', () => { new Feature({ id: 'fooFeature', name: 'Foo Feature', - app: [], + app: ['fooApp'], navLinkId: 'foo', privileges: null, }), @@ -63,6 +63,7 @@ describe('usingPrivileges', () => { Object.freeze({ navLinks: { foo: true, + fooApp: true, bar: true, }, management: { @@ -85,6 +86,7 @@ describe('usingPrivileges', () => { expect(result).toEqual({ navLinks: { foo: false, + fooApp: false, bar: true, }, management: { diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 183ad9169a12..a9b3fa54d361 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -18,8 +18,12 @@ export function disableUICapabilitiesFactory( logger: Logger, authz: AuthorizationServiceSetup ) { + // nav links are sourced from two places: + // 1) The `navLinkId` property. This is deprecated and will be removed (https://github.com/elastic/kibana/issues/66217) + // 2) The apps property. The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship. + // This behavior is replacing the `navLinkId` property above. const featureNavLinkIds = features - .map((feature) => feature.navLinkId) + .flatMap((feature) => [feature.navLinkId, ...feature.app]) .filter((navLinkId) => navLinkId != null); const shouldDisableFeatureUICapability = ( 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 448b7b7e7ef4..8b5c119d5949 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 199b8a91e430..37b730885619 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/schema/common.ts b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts index 7f8c938d54fe..fdb2570314cd 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts @@ -6,6 +6,15 @@ import * as t from 'io-ts'; +export const compressionAlgorithm = t.keyof({ + none: null, + zlib: null, +}); + +export const encryptionAlgorithm = t.keyof({ + none: null, +}); + export const identifier = t.string; export const manifestVersion = t.string; @@ -15,8 +24,8 @@ export const manifestSchemaVersion = t.keyof({ }); export type ManifestSchemaVersion = t.TypeOf; +export const relativeUrl = t.string; + export const sha256 = t.string; export const size = t.number; - -export const url = t.string; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts index 470e9b13ef78..2f03895d91c7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts @@ -5,13 +5,26 @@ */ import * as t from 'io-ts'; -import { identifier, manifestSchemaVersion, manifestVersion, sha256, size, url } from './common'; +import { + compressionAlgorithm, + encryptionAlgorithm, + identifier, + manifestSchemaVersion, + manifestVersion, + relativeUrl, + sha256, + size, +} from './common'; export const manifestEntrySchema = t.exact( t.type({ - url, - sha256, - size, + relative_url: relativeUrl, + precompress_sha256: sha256, + precompress_size: size, + postcompress_sha256: sha256, + postcompress_size: size, + compression_algorithm: compressionAlgorithm, + encryption_algorithm: encryptionAlgorithm, }) ); diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 42b1337a9146..f2b8acb627cc 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/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index c8c18696359f..fd52fb6734ef 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,7 +30,8 @@ import { loginAndWaitForPage } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -describe('Alerts', () => { +// Flaky: https://github.com/elastic/kibana/issues/70727 +describe.skip('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 1ce5243bf795..703ef6584f16 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_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx index b6db6eb93d77..533a9d51a9bc 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import uuid from 'uuid'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { LegendItem } from '../../../common/components/charts/draggable_legend_item'; @@ -28,7 +29,7 @@ import { alertsHistogramOptions } from './config'; import { formatAlertsData, getAlertsHistogramQuery, showInitialLoadingSpinner } from './helpers'; import { AlertsHistogram } from './alerts_histogram'; import * as i18n from './translations'; -import { RegisterQuery, AlertsHistogramOption, AlertsAggregation, AlertsTotal } from './types'; +import { AlertsHistogramOption, AlertsAggregation, AlertsTotal } from './types'; import { LinkButton } from '../../../common/components/links'; import { SecurityPageName } from '../../../app/types'; @@ -52,12 +53,11 @@ const ViewAlertsFlexItem = styled(EuiFlexItem)` margin-left: 24px; `; -interface AlertsHistogramPanelProps { +interface AlertsHistogramPanelProps + extends Pick { chartHeight?: number; defaultStackByOption?: AlertsHistogramOption; - deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; - from: number; headerChildren?: React.ReactNode; /** Override all defaults, and only display this field */ onlyField?: string; @@ -65,12 +65,11 @@ interface AlertsHistogramPanelProps { legendPosition?: Position; panelHeight?: number; signalIndexName: string | null; - setQuery: (params: RegisterQuery) => void; showLinkToAlerts?: boolean; showTotalAlertsCount?: boolean; stackByOptions?: AlertsHistogramOption[]; + timelineId?: string; title?: string; - to: number; updateDateRange: UpdateDateRange; } @@ -98,8 +97,9 @@ export const AlertsHistogramPanel = memo( showLinkToAlerts = false, showTotalAlertsCount = false, stackByOptions, - to, + timelineId, title = i18n.HISTOGRAM_HEADER, + to, updateDateRange, }) => { // create a unique, but stable (across re-renders) query id @@ -163,11 +163,12 @@ export const AlertsHistogramPanel = memo( `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` ), field: selectedStackByOption.value, + timelineId, value: bucket.key, })) : NO_LEGEND_DATA, // eslint-disable-next-line react-hooks/exhaustive-deps - [alertsData, selectedStackByOption.value] + [alertsData, selectedStackByOption.value, timelineId] ); useEffect(() => { 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 a3972fd35bf2..4b57c7dc20d9 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/alerts/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx index a3e76557a6ff..78a18dc336e5 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx @@ -22,3 +22,5 @@ DetectionEngineHeaderPageComponent.defaultProps = { }; export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent); + +DetectionEngineHeaderPage.displayName = 'DetectionEngineHeaderPage'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap index 549f0590681b..1ed55774f935 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap @@ -27,6 +27,7 @@ exports[`RuleActionsOverflow snapshots renders correctly against snapshot 1`] = isOpen={false} ownFocus={true} panelPaddingSize="none" + repositionOnScroll={true} > diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx index d033bc25e980..fa7c85c95d87 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx @@ -17,6 +17,11 @@ import { useWithSource } from '../../../common/containers/source'; jest.mock('../../components/user_info'); jest.mock('../../../common/containers/source'); jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest + .fn() + .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), +})); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx index dc0b22c82af3..5c525a855347 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx @@ -12,7 +12,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; -import { GlobalTime } from '../../../common/containers/global_time'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useWithSource } from '../../../common/containers/source'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; @@ -44,6 +44,7 @@ export const DetectionEnginePageComponent: React.FC = ({ query, setAbsoluteRangeDatePicker, }) => { + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { loading, isSignalIndexExists, @@ -131,36 +132,28 @@ export const DetectionEnginePageComponent: React.FC = ({ - - {({ to, from, deleteQuery, setQuery }) => ( - <> - <> - - - - - - )} - + + + ) : ( diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index b453125223c3..fd75c229d479 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -61,6 +61,7 @@ export const TagsFilterPopoverComponent = ({ isOpen={isTagPopoverOpen} closePopover={() => setIsTagPopoverOpen(!isTagPopoverOpen)} panelPaddingSize="none" + repositionOnScroll > {tags.map((tag, index) => ( diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx index 0acb18082379..11099e8cfc75 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx @@ -18,6 +18,12 @@ import { useParams } from 'react-router-dom'; jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('../../../../../common/containers/source'); +jest.mock('../../../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest + .fn() + .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), +})); + jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -50,6 +56,6 @@ describe('RuleDetailsPageComponent', () => { } ); - expect(wrapper.find('GlobalTime')).toHaveLength(1); + expect(wrapper.find('DetectionEngineHeaderPage')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx index 2ec603546983..b937e95c0a57 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -56,7 +56,7 @@ import { StepPanel } from '../../../../components/rules/step_panel'; import { getStepsData, redirectToDetections, userHasNoPermissions } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; -import { GlobalTime } from '../../../../../common/containers/global_time'; +import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { State } from '../../../../../common/store'; @@ -103,6 +103,7 @@ export const RuleDetailsPageComponent: FC = ({ query, setAbsoluteRangeDatePicker, }) => { + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { loading, isSignalIndexExists, @@ -263,169 +264,164 @@ export const RuleDetailsPageComponent: FC = ({ {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions(canUserCRUD) && } {indicesExist ? ( - - {({ to, from, deleteQuery, setQuery }) => ( - - - - - - - - {detectionI18n.LAST_ALERT} - {': '} - {lastAlerts} - , - ] - : []), - , - ]} - title={title} - > - + + + + + + + + {detectionI18n.LAST_ALERT} + {': '} + {lastAlerts} + , + ] + : []), + , + ]} + title={title} + > + + + + + + + + + - - - + {ruleI18n.EDIT_RULE_SETTINGS} + - - - - - {ruleI18n.EDIT_RULE_SETTINGS} - - - - - - + - - {ruleError} - - - - + + + + {ruleError} + + + + + + + + + + + {defineRuleData != null && ( + + )} + - - - - - - {defineRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - - )} - - - + + + + {scheduleRuleData != null && ( + + )} + + + + + {tabs} + + {ruleDetailTab === RuleDetailTabs.alerts && ( + <> + - {tabs} - - {ruleDetailTab === RuleDetailTabs.alerts && ( - <> - - - {ruleId != null && ( - - )} - - )} - {ruleDetailTab === RuleDetailTabs.exceptions && ( - )} - {ruleDetailTab === RuleDetailTabs.failures && } - - - )} - + + )} + {ruleDetailTab === RuleDetailTabs.exceptions && ( + + )} + {ruleDetailTab === RuleDetailTabs.failures && } + + ) : ( diff --git a/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx b/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx index 7b66bcffc89a..4c16a8c0f324 100644 --- a/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx @@ -84,6 +84,7 @@ export const FilterPopoverComponent = ({ isOpen={isPopoverOpen} closePopover={setIsPopoverOpenCb} panelPaddingSize="none" + repositionOnScroll > {options.map((option, index) => ( diff --git a/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx index 6b8e00921abc..29f1a2c5a149 100644 --- a/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx @@ -71,6 +71,7 @@ export const PropertyActions = React.memo(({ propertyActio id="settingsPopover" isOpen={showActions} closePopover={onClosePopover} + repositionOnScroll > = ({ barChart, configs, stackByField, + timelineId, }) => { const { ref: measureRef, width, height } = useThrottledResizeObserver(); const legendItems: LegendItem[] = useMemo( @@ -135,11 +137,12 @@ export const BarChartComponent: React.FC = ({ dataProviderId: escapeDataProviderId( `draggable-legend-item-${uuid.v4()}-${stackByField}-${d.key}` ), + timelineId, field: stackByField, value: d.key, })) : NO_LEGEND_DATA, - [barChart, stackByField] + [barChart, stackByField, timelineId] ); const customHeight = get('customHeight', configs); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx index cdda1733932d..bb71e5e73475 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx @@ -21,13 +21,14 @@ export interface LegendItem { color?: string; dataProviderId: string; field: string; + timelineId?: string; value: string; } const DraggableLegendItemComponent: React.FC<{ legendItem: LegendItem; }> = ({ legendItem }) => { - const { color, dataProviderId, field, value } = legendItem; + const { color, dataProviderId, field, timelineId, value } = legendItem; return ( @@ -44,6 +45,7 @@ const DraggableLegendItemComponent: React.FC<{ data-test-subj={`legend-item-${dataProviderId}`} field={field} id={dataProviderId} + timelineId={timelineId} value={value} /> ) : ( diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index 3edc1d0d84b6..74efe2d34fcc 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -18,11 +18,10 @@ import { IdToDataProvider } from '../../store/drag_and_drop/model'; import { State } from '../../store/types'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { reArrangeProviders } from '../../../timelines/components/timeline/data_providers/helpers'; -import { ACTIVE_TIMELINE_REDUX_ID } from '../top_n'; import { ADDED_TO_TIMELINE_MESSAGE } from '../../hooks/translations'; import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline'; import { displaySuccessToast, useStateToaster } from '../toasters'; - +import { TimelineId } from '../../../../common/types/timeline'; import { addFieldToTimelineColumns, addProviderToTimeline, @@ -35,7 +34,7 @@ import { userIsReArrangingProviders, } from './helpers'; -// @ts-ignore +// @ts-expect-error window['__react-beautiful-dnd-disable-dev-warnings'] = true; interface Props { @@ -67,7 +66,7 @@ const onDragEndHandler = ({ destination: result.destination, dispatch, source: result.source, - timelineId: ACTIVE_TIMELINE_REDUX_ID, + timelineId: TimelineId.active, }); } else if (providerWasDroppedOnTimeline(result)) { addProviderToTimeline({ @@ -76,7 +75,7 @@ const onDragEndHandler = ({ dispatch, onAddedToTimeline, result, - timelineId: ACTIVE_TIMELINE_REDUX_ID, + timelineId: TimelineId.active, }); } else if (fieldWasDroppedOnTimelineColumns(result)) { addFieldToTimelineColumns({ @@ -130,7 +129,6 @@ export const DragDropContextWrapperComponent = React.memo {children} @@ -152,7 +150,7 @@ const emptyActiveTimelineDataProviders: DataProvider[] = []; // stable reference const mapStateToProps = (state: State) => { const activeTimelineDataProviders = - timelineSelectors.getTimelineByIdSelector()(state, ACTIVE_TIMELINE_REDUX_ID)?.dataProviders ?? + timelineSelectors.getTimelineByIdSelector()(state, TimelineId.active)?.dataProviders ?? emptyActiveTimelineDataProviders; const dataProviders = dragAndDropSelectors.dataProvidersSelector(state) ?? emptyDataProviders; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 22b95f0d0c0e..e7594365e810 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { Draggable, DraggableProvided, @@ -22,7 +22,7 @@ import { DataProvider } from '../../../timelines/components/timeline/data_provid import { TruncatableText } from '../truncatable_text'; import { WithHoverActions } from '../with_hover_actions'; -import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; +import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content'; import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; @@ -76,6 +76,7 @@ interface Props { dataProvider: DataProvider; inline?: boolean; render: RenderFunctionProp; + timelineId?: string; truncate?: boolean; onFilterAdded?: () => void; } @@ -100,16 +101,31 @@ export const getStyle = ( }; export const DraggableWrapper = React.memo( - ({ dataProvider, onFilterAdded, render, truncate }) => { + ({ dataProvider, onFilterAdded, render, timelineId, truncate }) => { + const draggableRef = useRef(null); + const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); const [showTopN, setShowTopN] = useState(false); - const toggleTopN = useCallback(() => { - setShowTopN(!showTopN); - }, [setShowTopN, showTopN]); - + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); const [providerRegistered, setProviderRegistered] = useState(false); const dispatch = useDispatch(); + const handleClosePopOverTrigger = useCallback( + () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger), + [] + ); + + const toggleTopN = useCallback(() => { + setShowTopN((prevShowTopN) => { + const newShowTopN = !prevShowTopN; + if (newShowTopN === false) { + handleClosePopOverTrigger(); + } + return newShowTopN; + }); + }, [handleClosePopOverTrigger]); + const registerProvider = useCallback(() => { if (!providerRegistered) { dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); @@ -126,17 +142,19 @@ export const DraggableWrapper = React.memo( () => () => { unRegisterProvider(); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [unRegisterProvider] ); const hoverContent = useMemo( () => ( ( } /> ), - [dataProvider, onFilterAdded, showTopN, toggleTopN] + [ + dataProvider, + handleClosePopOverTrigger, + onFilterAdded, + showTopN, + timelineId, + timelineIdFind, + toggleTopN, + ] ); const renderContent = useCallback( @@ -184,7 +210,10 @@ export const DraggableWrapper = React.memo( { + provided.innerRef(e); + draggableRef.current = e; + }} data-test-subj="providerContainer" isDragging={snapshot.isDragging} registerProvider={registerProvider} @@ -214,7 +243,12 @@ export const DraggableWrapper = React.memo( ); return ( - + ); }, (prevProps, nextProps) => diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index ee1dc73b27fe..3507b0f8c447 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -52,6 +52,7 @@ jest.mock('../../../timelines/components/manage_timeline', () => { return { ...original, useManageTimeline: () => ({ + getManageTimelineById: jest.fn().mockReturnValue({ indexToAdd: [] }), getTimelineFilterManager: mockGetTimelineFilterManager, isManagedTimeline: jest.fn().mockReturnValue(false), }), @@ -63,8 +64,10 @@ const timelineId = TimelineId.active; const field = 'process.name'; const value = 'nice'; const toggleTopN = jest.fn(); +const goGetTimelineId = jest.fn(); const defaultProps = { field, + goGetTimelineId, showTopN: false, timelineId, toggleTopN, @@ -130,6 +133,18 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() ).toBe(false); }); + + test(`it should call goGetTimelineId when user is over the 'Filter ${hoverAction} value' button`, () => { + const wrapper = mount( + + + + ); + const button = wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first(); + button.simulate('mouseenter'); + expect(goGetTimelineId).toHaveBeenCalledWith(true); + }); + describe('when run in the context of a timeline', () => { let wrapper: ReactWrapper; let onFilterAdded: () => void; @@ -151,6 +166,7 @@ describe('DraggableWrapperHoverContent', () => { ); }); + test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); @@ -459,6 +475,24 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); }); + test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + ); + const button = wrapper.find(`[data-test-subj="show-top-field"]`).first(); + button.simulate('mouseenter'); + expect(goGetTimelineId).toHaveBeenCalledWith(true); + }); + test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index 4efdea5eee43..a951bfa98d64 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; import { getAllFieldsByName, useWithSource } from '../../containers/source'; @@ -19,20 +19,25 @@ import { allowTopN } from './helpers'; import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; +import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../../../timelines/components/timeline/styles'; interface Props { + closePopOver?: () => void; draggableId?: DraggableId; field: string; + goGetTimelineId?: (args: boolean) => void; onFilterAdded?: () => void; showTopN: boolean; - timelineId?: string; + timelineId?: string | null; toggleTopN: () => void; value?: string[] | string | null; } const DraggableWrapperHoverContentComponent: React.FC = ({ + closePopOver, draggableId, field, + goGetTimelineId, onFilterAdded, showTopN, timelineId, @@ -44,17 +49,37 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); - const { getTimelineFilterManager } = useManageTimeline(); + const { getManageTimelineById, getTimelineFilterManager } = useManageTimeline(); const filterManager = useMemo( () => - timelineId === TimelineId.active || - (draggableId != null && draggableId?.includes(TimelineId.active)) + timelineId === TimelineId.active ? getTimelineFilterManager(TimelineId.active) : filterManagerBackup, - [draggableId, timelineId, getTimelineFilterManager, filterManagerBackup] + [timelineId, getTimelineFilterManager, filterManagerBackup] ); + // Regarding data from useManageTimeline: + // * `indexToAdd`, which enables the alerts index to be appended to + // the `indexPattern` returned by `useWithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the alerts index + // to the index pattern. + const { indexToAdd } = useMemo( + () => + timelineId === TimelineId.active + ? getManageTimelineById(TimelineId.active) + : { indexToAdd: null }, + [getManageTimelineById, timelineId] + ); + + const handleStartDragToTimeline = useCallback(() => { + startDragToTimeline(); + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, startDragToTimeline]); + const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); @@ -62,13 +87,15 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ if (activeFilterManager != null) { activeFilterManager.addFilters(filter); - + if (closePopOver != null) { + closePopOver(); + } if (onFilterAdded != null) { onFilterAdded(); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [field, value, filterManager, onFilterAdded]); + }, [closePopOver, field, value, filterManager, onFilterAdded]); const filterOutValue = useCallback(() => { const filter = @@ -78,14 +105,23 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ if (activeFilterManager != null) { activeFilterManager.addFilters(filter); + if (closePopOver != null) { + closePopOver(); + } if (onFilterAdded != null) { onFilterAdded(); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [field, value, filterManager, onFilterAdded]); + }, [closePopOver, field, value, filterManager, onFilterAdded]); - const { browserFields } = useWithSource(); + const handleGoGetTimelineId = useCallback(() => { + if (goGetTimelineId != null && timelineId == null) { + goGetTimelineId(true); + } + }, [goGetTimelineId, timelineId]); + + const { browserFields, indexPattern } = useWithSource('default', indexToAdd); return ( <> @@ -97,6 +133,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ data-test-subj="filter-for-value" iconType="magnifyWithPlus" onClick={filterForValue} + onMouseEnter={handleGoGetTimelineId} /> )} @@ -109,6 +146,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ data-test-subj="filter-out-value" iconType="magnifyWithMinus" onClick={filterOutValue} + onMouseEnter={handleGoGetTimelineId} /> )} @@ -120,7 +158,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ color="text" data-test-subj="add-to-timeline" iconType="timeline" - onClick={startDragToTimeline} + onClick={handleStartDragToTimeline} /> )} @@ -139,6 +177,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ data-test-subj="show-top-field" iconType="visBarVertical" onClick={toggleTopN} + onMouseEnter={handleGoGetTimelineId} /> )} @@ -147,7 +186,10 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ @@ -172,3 +214,30 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ DraggableWrapperHoverContentComponent.displayName = 'DraggableWrapperHoverContentComponent'; export const DraggableWrapperHoverContent = React.memo(DraggableWrapperHoverContentComponent); + +export const useGetTimelineId = function ( + elem: React.MutableRefObject, + getTimelineId: boolean = false +) { + const [timelineId, setTimelineId] = useState(null); + + useEffect(() => { + let startElem: Element | (Node & ParentNode) | null = elem.current; + if (startElem != null && getTimelineId) { + for (; startElem && startElem !== document; startElem = startElem.parentNode) { + const myElem: Element = startElem as Element; + if ( + myElem != null && + myElem.classList != null && + myElem.classList.contains(SELECTOR_TIMELINE_BODY_CLASS_NAME) && + myElem.hasAttribute('data-timeline-id') + ) { + setTimelineId(myElem.getAttribute('data-timeline-id')); + break; + } + } + } + }, [elem, getTimelineId]); + + return timelineId; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx index fcf007a4cf1b..62a07550650a 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx @@ -21,6 +21,7 @@ export interface DefaultDraggableType { name?: string | null; queryValue?: string | null; children?: React.ReactNode; + timelineId?: string; tooltipContent?: React.ReactNode; } @@ -83,7 +84,7 @@ Content.displayName = 'Content'; * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ export const DefaultDraggable = React.memo( - ({ id, field, value, name, children, tooltipContent, queryValue }) => + ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => value != null ? ( ( ) } + timelineId={timelineId} /> ) : null ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index e01ccf1e544b..7b6e9fb21a3e 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -147,7 +147,6 @@ export const getColumns = ({ data-test-subj="field-name" fieldId={field} onUpdateColumns={onUpdateColumns} - timelineId={contextId} />
)} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx index 2920f1a85eee..afc6d55de364 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx @@ -100,6 +100,7 @@ const ExceptionsViewerPaginationComponent = ({ isOpen={isOpen} closePopover={handleClosePerPageMenu} panelPaddingSize="none" + repositionOnScroll > diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 3e196c4b7bad..16fe2a6669ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -31,7 +31,7 @@ import { GetTitle, GetSubTitle, } from '../../components/matrix_histogram/types'; -import { SetQuery } from '../../../hosts/pages/navigation/types'; +import { GlobalTimeArgs } from '../../containers/use_global_time'; import { QueryTemplateProps } from '../../containers/query_template'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { InputsModelId } from '../../store/inputs/constants'; @@ -48,11 +48,12 @@ export interface OwnProps extends QueryTemplateProps { legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; showSpacer?: boolean; - setQuery: SetQuery; + setQuery: GlobalTimeArgs['setQuery']; setAbsoluteRangeDatePickerTarget?: InputsModelId; showLegend?: boolean; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; + timelineId?: string; title: string | GetTitle; type: hostsModel.HostsType | networkModel.NetworkType; } @@ -94,6 +95,7 @@ export const MatrixHistogramComponent: React.FC< stackByOptions, startDate, subtitle, + timelineId, title, titleSize, dispatchSetAbsoluteRangeDatePicker, @@ -242,6 +244,7 @@ export const MatrixHistogramComponent: React.FC< barChart={barChartData} configs={barchartConfigs} stackByField={selectedStackByOption.value} + timelineId={timelineId} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index a9e6cdd19bb2..ff0816758cb0 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -8,10 +8,10 @@ import { EuiTitleSize } from '@elastic/eui'; import { ScaleType, Position, TickFormatter } from '@elastic/charts'; import { ActionCreator } from 'redux'; import { ESQuery } from '../../../../common/typed_json'; -import { SetQuery } from '../../../hosts/pages/navigation/types'; import { InputsModelId } from '../../store/inputs/constants'; import { HistogramType } from '../../../graphql/types'; import { UpdateDateRange } from '../charts/common'; +import { GlobalTimeArgs } from '../../containers/use_global_time'; export type MatrixHistogramMappingTypes = Record< string, @@ -47,15 +47,15 @@ interface MatrixHistogramBasicProps { from: number; to: number; }>; - endDate: number; + endDate: GlobalTimeArgs['to']; headerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; id: string; legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; panelHeight?: number; - setQuery: SetQuery; - startDate: number; + setQuery: GlobalTimeArgs['setQuery']; + startDate: GlobalTimeArgs['from']; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; title?: string | GetTitle; @@ -80,11 +80,12 @@ export interface MatrixHistogramQueryProps { } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { + legendPosition?: Position; scaleType?: ScaleType; - yTickFormatter?: (value: number) => string; showLegend?: boolean; showSpacer?: boolean; - legendPosition?: Position; + timelineId?: string; + yTickFormatter?: (value: number) => string; } export interface HistogramBucket { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap index a9ec474a7b68..6694cec53987 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap @@ -95,6 +95,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` onClick={[Function]} ownFocus={false} panelPaddingSize="m" + repositionOnScroll={true} > setIsOpen(!isOpen)} closePopover={() => setIsOpen(!isOpen)} button={} + repositionOnScroll > setIsGroupPopoverOpen(!isGroupPopoverOpen)} panelPaddingSize="none" + repositionOnScroll > {uniqueGroups.map((group, index) => ( { } isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(!isPopoverOpen)} + repositionOnScroll > @@ -147,6 +148,7 @@ export const MlPopover = React.memo(() => { } isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(!isPopoverOpen)} + repositionOnScroll > {i18n.ML_JOB_SETTINGS} diff --git a/x-pack/plugins/security_solution/public/common/components/page/manage_query.tsx b/x-pack/plugins/security_solution/public/common/components/page/manage_query.tsx index 9e78f704b0f0..02d9a62f2890 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/manage_query.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/manage_query.tsx @@ -9,16 +9,14 @@ import { omit } from 'lodash/fp'; import React from 'react'; import { inputsModel } from '../../store'; -import { SetQuery } from '../../../hosts/pages/navigation/types'; +import { GlobalTimeArgs } from '../../containers/use_global_time'; -interface OwnProps { - deleteQuery?: ({ id }: { id: string }) => void; +interface OwnProps extends Pick { headerChildren?: React.ReactNode; id: string; legendPosition?: Position; loading: boolean; refetch: inputsModel.Refetch; - setQuery: SetQuery; inspect?: inputsModel.InspectQuery; } diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 3b3130af77cf..9f95284d989a 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -273,6 +273,7 @@ const PaginatedTableComponent: FC = ({ isOpen={isPopoverOpen} closePopover={closePopover} panelPaddingSize="none" + repositionOnScroll > 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 f079715baec1..a3cab1cfabd7 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/common/components/tables/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx index b8ea32969c01..55e575877550 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx @@ -195,6 +195,7 @@ export const PopoverComponent = ({ closePopover={() => setIsOpen(!isOpen)} id={`${idPrefix}-popover`} isOpen={isOpen} + repositionOnScroll > {children} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 336f906b3bed..503e9983692f 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -15,17 +15,19 @@ import { SUB_PLUGINS_REDUCER, kibanaObservable, createSecuritySolutionStorageMock, + mockIndexPattern, } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { createStore, State } from '../../store'; import { Props } from './top_n'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; +import { StatefulTopN } from '.'; import { ManageGlobalTimeline, timelineDefaults, } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -94,9 +96,9 @@ const state: State = { timeline: { ...mockGlobalState.timeline, timelineById: { - [ACTIVE_TIMELINE_REDUX_ID]: { + [TimelineId.active]: { ...mockGlobalState.timeline.timelineById.test, - id: ACTIVE_TIMELINE_REDUX_ID, + id: TimelineId.active, dataProviders: [ { id: @@ -189,6 +191,9 @@ describe('StatefulTopN', () => { { beforeEach(() => { filterManager = new FilterManager(mockUiSettingsForFilterManager); const manageTimelineForTesting = { - [ACTIVE_TIMELINE_REDUX_ID]: { + [TimelineId.active]: { ...timelineDefaults, - id: ACTIVE_TIMELINE_REDUX_ID, + id: TimelineId.active, filterManager, }, }; @@ -278,6 +283,9 @@ describe('StatefulTopN', () => { { const filterManager = new FilterManager(mockUiSettingsForFilterManager); const manageTimelineForTesting = { - [ACTIVE_TIMELINE_REDUX_ID]: { + [TimelineId.active]: { ...timelineDefaults, - id: ACTIVE_TIMELINE_REDUX_ID, + id: TimelineId.active, filterManager, documentType: 'alerts', }, @@ -356,6 +364,9 @@ describe('StatefulTopN', () => { { // filters that appear at the top of most views in the app, and all the // filters in the active timeline: const mapStateToProps = (state: State) => { - const activeTimeline: TimelineModel = - getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; + const activeTimeline: TimelineModel = getTimeline(state, TimelineId.active) ?? timelineDefaults; const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); @@ -48,7 +49,7 @@ const makeMapStateToProps = () => { activeTimelineEventType: activeTimeline.eventType, activeTimelineFilters, activeTimelineFrom: activeTimelineInput.timerange.from, - activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), + activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, TimelineId.active), activeTimelineTo: activeTimelineInput.timerange.to, dataProviders: activeTimeline.dataProviders, globalQuery: getGlobalQuerySelector(state), @@ -64,9 +65,17 @@ const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRang const connector = connect(makeMapStateToProps, mapDispatchToProps); +// * `indexToAdd`, which enables the alerts index to be appended to +// the `indexPattern` returned by `useWithSource`, may only be populated when +// this component is rendered in the context of the active timeline. This +// behavior enables the 'All events' view by appending the alerts index +// to the index pattern. interface OwnProps { browserFields: BrowserFields; field: string; + indexPattern: IIndexPattern; + indexToAdd: string[] | null; + timelineId?: string; toggleTopN: () => void; onFilterAdded?: () => void; value?: string[] | string | null; @@ -83,86 +92,67 @@ const StatefulTopNComponent: React.FC = ({ browserFields, dataProviders, field, + indexPattern, + indexToAdd, globalFilters = EMPTY_FILTERS, globalQuery = EMPTY_QUERY, kqlMode, onFilterAdded, setAbsoluteRangeDatePicker, + timelineId, toggleTopN, value, }) => { const kibana = useKibana(); - - // Regarding data from useTimelineTypeContext: - // * `documentType` (e.g. 'alerts') may only be populated in some views, - // e.g. the `Alerts` view on the `Detections` page. - // * `id` (`timelineId`) may only be populated when we are rendered in the - // context of the active timeline. - // * `indexToAdd`, which enables the alerts index to be appended to - // the `indexPattern` returned by `useWithSource`, may only be populated when - // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the alerts index - // to the index pattern. - const { isManagedTimeline, getManageTimelineById } = useManageTimeline(); - const { documentType, id: timelineId, indexToAdd } = useMemo( - () => - isManagedTimeline(ACTIVE_TIMELINE_REDUX_ID) - ? getManageTimelineById(ACTIVE_TIMELINE_REDUX_ID) - : { documentType: null, id: null, indexToAdd: null }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [getManageTimelineById] - ); + const { from, deleteQuery, setQuery, to } = useGlobalTime(); const options = getOptions( - timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined + timelineId === TimelineId.active ? activeTimelineEventType : undefined ); - const { indexPattern } = useWithSource('default', indexToAdd); - return ( - - {({ from, deleteQuery, setQuery, to }) => ( - - )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 0ccb7e1e72f1..5e2fd998224c 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -5,14 +5,14 @@ */ import { EuiButtonIcon, EuiSuperSelect } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; +import { GlobalTimeArgs } from '../../containers/use_global_time'; import { EventsByDataset } from '../../../overview/components/events_by_dataset'; import { SignalsByCategory } from '../../../overview/components/signals_by_category'; import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { EventType } from '../../../timelines/store/timeline/model'; @@ -43,13 +43,11 @@ const TopNContent = styled.div` } `; -export interface Props { +export interface Props extends Pick { combinedQueries?: string; defaultView: EventType; - deleteQuery?: ({ id }: { id: string }) => void; field: string; filters: Filter[]; - from: number; indexPattern: IIndexPattern; indexToAdd?: string[] | null; options: TopNOption[]; @@ -60,13 +58,7 @@ export interface Props { to: number; }>; setAbsoluteRangeDatePickerTarget: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; + timelineId?: string; toggleTopN: () => void; onFilterAdded?: () => void; value?: string[] | string | null; @@ -89,12 +81,17 @@ const TopNComponent: React.FC = ({ setAbsoluteRangeDatePicker, setAbsoluteRangeDatePickerTarget, setQuery, + timelineId, to, toggleTopN, }) => { const [view, setView] = useState(defaultView); const onViewSelected = useCallback((value: string) => setView(value as EventType), [setView]); + useEffect(() => { + setView(defaultView); + }, [defaultView]); + const headerChildren = useMemo( () => ( = ({ setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} showSpacer={false} + timelineId={timelineId} to={to} /> ) : ( @@ -145,6 +143,7 @@ const TopNComponent: React.FC = ({ setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} + timelineId={timelineId} to={to} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx index 250ed75f134c..f072b27274ed 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx @@ -33,6 +33,7 @@ const Popover = React.memo( } closePopover={() => setPopoverState(false)} isOpen={popoverState} + repositionOnScroll > {popoverContent?.(closePopover)} diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx index 8679dae44833..361779a4a33b 100644 --- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx @@ -5,7 +5,7 @@ */ import { EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; @@ -22,6 +22,7 @@ interface Props { * Always show the hover menu contents (default: false) */ alwaysShow?: boolean; + closePopOverTrigger?: boolean; /** * The contents of the hover menu. It is highly recommended you wrap this * content in a `div` with `position: absolute` to prevent it from effecting @@ -47,7 +48,8 @@ interface Props { * provides a signal to the content that the user is in a hover state. */ export const WithHoverActions = React.memo( - ({ alwaysShow = false, hoverContent, render }) => { + ({ alwaysShow = false, closePopOverTrigger, hoverContent, render }) => { + const [isOpen, setIsOpen] = useState(hoverContent != null && alwaysShow); const [showHoverContent, setShowHoverContent] = useState(false); const onMouseEnter = useCallback(() => { // NOTE: the following read from the DOM is expensive, but not as @@ -64,10 +66,16 @@ export const WithHoverActions = React.memo( const content = useMemo(() => <>{render(showHoverContent)}, [render, showHoverContent]); - const isOpen = hoverContent != null && (showHoverContent || alwaysShow); + useEffect(() => { + setIsOpen(hoverContent != null && (showHoverContent || alwaysShow)); + }, [hoverContent, showHoverContent, alwaysShow]); - const popover = useMemo(() => { - return ( + useEffect(() => { + setShowHoverContent(false); + }, [closePopOverTrigger]); + + return ( +
( isOpen={isOpen} panelPaddingSize={!alwaysShow ? 's' : 'none'} > - {isOpen ? hoverContent : null} + {isOpen ? <>{hoverContent} : null} - ); - }, [content, onMouseLeave, isOpen, alwaysShow, hoverContent]); - - return ( -
- {popover}
); } diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts index a2009809a991..d716df70246f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts @@ -7,7 +7,7 @@ import { ESTermQuery } from '../../../../../common/typed_json'; import { NarrowDateRange } from '../../../components/ml/types'; import { UpdateDateRange } from '../../../components/charts/common'; -import { SetQuery } from '../../../../hosts/pages/navigation/types'; +import { GlobalTimeArgs } from '../../use_global_time'; import { FlowTarget } from '../../../../graphql/types'; import { HostsType } from '../../../../hosts/store/model'; import { NetworkType } from '../../../../network/store//model'; @@ -22,11 +22,11 @@ export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { // eslint-disable-next-line @typescript-eslint/no-explicit-any AnomaliesTableComponent: React.NamedExoticComponent; deleteQuery?: ({ id }: { id: string }) => void; - endDate: number; + endDate: GlobalTimeArgs['to']; flowTarget?: FlowTarget; narrowDateRange: NarrowDateRange; - setQuery: SetQuery; - startDate: number; + setQuery: GlobalTimeArgs['setQuery']; + startDate: GlobalTimeArgs['from']; skip: boolean; updateDateRange?: UpdateDateRange; hideHistogramIfEmpty?: boolean; diff --git a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx deleted file mode 100644 index 9c9778c7074e..000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx +++ /dev/null @@ -1,98 +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, { useCallback, useState, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { inputsModel, inputsSelectors, State } from '../../store'; -import { inputsActions } from '../../store/actions'; - -interface SetQuery { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch | inputsModel.RefetchKql; -} - -export interface GlobalTimeArgs { - from: number; - to: number; - setQuery: ({ id, inspect, loading, refetch }: SetQuery) => void; - deleteQuery?: ({ id }: { id: string }) => void; - isInitializing: boolean; -} - -interface OwnProps { - children: (args: GlobalTimeArgs) => React.ReactNode; -} - -type GlobalTimeProps = OwnProps & PropsFromRedux; - -export const GlobalTimeComponent: React.FC = ({ - children, - deleteAllQuery, - deleteOneQuery, - from, - to, - setGlobalQuery, -}) => { - const [isInitializing, setIsInitializing] = useState(true); - - const setQuery = useCallback( - ({ id, inspect, loading, refetch }: SetQuery) => - setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), - [setGlobalQuery] - ); - - const deleteQuery = useCallback( - ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), - [deleteOneQuery] - ); - - useEffect(() => { - if (isInitializing) { - setIsInitializing(false); - } - return () => { - deleteAllQuery({ id: 'global' }); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <> - {children({ - isInitializing, - from, - to, - setQuery, - deleteQuery, - })} - - ); -}; - -const mapStateToProps = (state: State) => { - const timerange: inputsModel.TimeRange = inputsSelectors.globalTimeRangeSelector(state); - return { - from: timerange.from, - to: timerange.to, - }; -}; - -const mapDispatchToProps = { - deleteAllQuery: inputsActions.deleteAllQuery, - deleteOneQuery: inputsActions.deleteOneQuery, - setGlobalQuery: inputsActions.setQuery, -}; - -export const connector = connect(mapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const GlobalTime = connector(React.memo(GlobalTimeComponent)); - -GlobalTime.displayName = 'GlobalTime'; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 9aa3b007511a..4f42f20c45ae 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -97,7 +97,7 @@ export const useWithSource = ( const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...(!onlyCheckIndexToAdd ? configIndex : []), ...indexToAdd]; + return onlyCheckIndexToAdd ? indexToAdd : [...configIndex, ...indexToAdd]; } return configIndex; }, [configIndex, indexToAdd, onlyCheckIndexToAdd]); @@ -135,41 +135,32 @@ export const useWithSource = ( }, }, }); - if (!isSubscribed) { - return setState((prevState) => ({ - ...prevState, + + if (isSubscribed) { + setState({ loading: false, - })); + indicesExist: indicesExistOrDataTemporarilyUnavailable( + get('data.source.status.indicesExist', result) + ), + browserFields: getBrowserFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + indexPattern: getIndexFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + errorMessage: null, + }); } - - setState({ - loading: false, - indicesExist: indicesExistOrDataTemporarilyUnavailable( - get('data.source.status.indicesExist', result) - ), - browserFields: getBrowserFields( - defaultIndex.join(), - get('data.source.status.indexFields', result) - ), - indexPattern: getIndexFields( - defaultIndex.join(), - get('data.source.status.indexFields', result) - ), - errorMessage: null, - }); } catch (error) { - if (!isSubscribed) { - return setState((prevState) => ({ + if (isSubscribed) { + setState((prevState) => ({ ...prevState, loading: false, + errorMessage: error.message, })); } - - setState((prevState) => ({ - ...prevState, - loading: false, - errorMessage: error.message, - })); } } diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx new file mode 100644 index 000000000000..9d5f1740b027 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { useGlobalTime } from '.'; + +jest.mock('react-redux', () => { + const originalModule = jest.requireActual('react-redux'); + + return { + ...originalModule, + useDispatch: jest.fn().mockReturnValue(jest.fn()), + useSelector: jest.fn().mockReturnValue({ from: 0, to: 0 }), + }; +}); + +describe('useGlobalTime', () => { + test('returns memoized value', () => { + const { result, rerender } = renderHook(() => useGlobalTime()); + + const result1 = result.current; + act(() => rerender()); + const result2 = result.current; + + expect(result1).toBe(result2); + expect(result1.from).toBe(0); + expect(result1.to).toBe(0); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx new file mode 100644 index 000000000000..b63616ecbcf5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { inputsSelectors } from '../../store'; +import { inputsActions } from '../../store/actions'; +import { SetQuery, DeleteQuery } from './types'; + +export const useGlobalTime = () => { + const dispatch = useDispatch(); + const { from, to } = useSelector(inputsSelectors.globalTimeRangeSelector); + const [isInitializing, setIsInitializing] = useState(true); + + const setQuery = useCallback( + ({ id, inspect, loading, refetch }: SetQuery) => + dispatch(inputsActions.setQuery({ inputId: 'global', id, inspect, loading, refetch })), + [dispatch] + ); + + const deleteQuery = useCallback( + ({ id }: DeleteQuery) => dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id })), + [dispatch] + ); + + useEffect(() => { + if (isInitializing) { + setIsInitializing(false); + } + return () => { + dispatch(inputsActions.deleteAllQuery({ id: 'global' })); + }; + }, [dispatch, isInitializing]); + + const memoizedReturn = useMemo( + () => ({ + isInitializing, + from, + to, + setQuery, + deleteQuery, + }), + [deleteQuery, from, isInitializing, setQuery, to] + ); + + return memoizedReturn; +}; + +export type GlobalTimeArgs = Omit, 'deleteQuery'> & + Partial, 'deleteQuery'>>; diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/types.ts b/x-pack/plugins/security_solution/public/common/containers/use_global_time/types.ts new file mode 100644 index 000000000000..9903c29202b2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/types.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 { inputsActions } from '../../store/actions'; + +export type SetQuery = Pick< + Parameters[0], + 'id' | 'inspect' | 'loading' | 'refetch' +>; + +export type DeleteQuery = Pick[0], 'id'>; diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts index 4e2b11b24e5a..e84438581fcd 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts @@ -26,33 +26,33 @@ describe('Kuery escape', () => { expect(escapeKuery(value)).to.be(expected); }); - it('should escape keywords', () => { + it('should NOT escape keywords', () => { const value = 'foo and bar or baz not qux'; - const expected = 'foo \\and bar \\or baz \\not qux'; + const expected = 'foo and bar or baz not qux'; expect(escapeKuery(value)).to.be(expected); }); - it('should escape keywords next to each other', () => { + it('should NOT escape keywords next to each other', () => { const value = 'foo and bar or not baz'; - const expected = 'foo \\and bar \\or \\not baz'; + const expected = 'foo and bar or not baz'; expect(escapeKuery(value)).to.be(expected); }); it('should not escape keywords without surrounding spaces', () => { const value = 'And this has keywords, or does it not?'; - const expected = 'And this has keywords, \\or does it not?'; + const expected = 'And this has keywords, or does it not?'; expect(escapeKuery(value)).to.be(expected); }); - it('should escape uppercase keywords', () => { + it('should NOT escape uppercase keywords', () => { const value = 'foo AND bar'; - const expected = 'foo \\AND bar'; + const expected = 'foo AND bar'; expect(escapeKuery(value)).to.be(expected); }); - it('should escape both keywords and special characters', () => { + it('should escape special characters and NOT keywords', () => { const value = 'Hello, "world", and to meet you!'; - const expected = 'Hello, \\"world\\", \\and to meet you!'; + const expected = 'Hello, \\"world\\", and to meet you!'; expect(escapeKuery(value)).to.be(expected); }); diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts index bd4d96a98c81..b06a6ec10f48 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts @@ -75,11 +75,12 @@ const escapeWhitespace = (val: string) => const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string // See the Keyword rule in kuery.peg -const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); +// I do not think that we need that anymore since we are doing a full match_phrase all the time now => return `"${escapeKuery(val)}"`; +// const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); -const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); +// const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); -export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); +export const escapeKuery = flow(escapeSpecialCharacters, escapeWhitespace); export const convertToBuildEsQuery = ({ config, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index e520facf285c..cce48a1e605b 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -21,6 +21,12 @@ jest.mock('../../../common/containers/source', () => ({ useWithSource: jest.fn().mockReturnValue({ indicesExist: true, indexPattern: mockIndexPattern }), })); +jest.mock('../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest + .fn() + .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), +})); + // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../common/components/search_bar', () => ({ diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index 505d0f37ca03..acde0cbe1d42 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -12,6 +12,7 @@ import { scoreIntervalToDateTime } from '../../../common/components/ml/score/sco import { Anomaly } from '../../../common/components/ml/types'; import { HostsTableType } from '../../store/model'; import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; import { AnomaliesHostTable } from '../../../common/components/ml/tables/anomalies_host_table'; import { HostDetailsTabsProps } from './types'; @@ -28,17 +29,13 @@ import { export const HostDetailsTabs = React.memo( ({ pageFilters, - deleteQuery, filterQuery, - from, - isInitializing, detailName, setAbsoluteRangeDatePicker, - setQuery, - to, indexPattern, hostDetailsPagePath, }) => { + const { from, to, isInitializing, deleteQuery, setQuery } = useGlobalTime(); const narrowDateRange = useCallback( (score: Anomaly, interval: string) => { const fromTo = scoreIntervalToDateTime(score, interval); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 1c66a9edc194..46823f037b61 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -27,6 +27,7 @@ import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { HostOverviewByNameQuery } from '../../containers/hosts/overview'; import { KpiHostDetailsQuery } from '../../containers/kpi_host_details'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useWithSource } from '../../../common/containers/source'; import { LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; @@ -50,17 +51,13 @@ const KpiHostDetailsManage = manageQuery(KpiHostsComponent); const HostDetailsComponent = React.memo( ({ filters, - from, - isInitializing, query, setAbsoluteRangeDatePicker, setHostDetailsTablesActivePageToZero, - setQuery, - to, detailName, - deleteQuery, hostDetailsPagePath, }) => { + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); useEffect(() => { setHostDetailsTablesActivePageToZero(); }, [setHostDetailsTablesActivePageToZero, detailName]); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts index aa6288d473c9..7a440964c31e 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts @@ -32,7 +32,7 @@ interface HostDetailsComponentDispatchProps extends HostBodyComponentDispatchPro setHostDetailsTablesActivePageToZero: ActionCreator; } -export interface HostDetailsProps extends HostsQueryProps { +export interface HostDetailsProps { detailName: string; hostDetailsPagePath: string; } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index 1ea3a3020a1d..566f8f23efd3 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -59,15 +59,8 @@ const mockHistory = { listen: jest.fn(), }; -const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); - describe('Hosts - rendering', () => { const hostProps: HostsComponentProps = { - from, - to, - setQuery: jest.fn(), - isInitializing: false, hostsPagePath: '', }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index f5cc651a3044..90438aec7c27 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -22,6 +22,7 @@ import { manageQuery } from '../../common/components/page/manage_query'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { KpiHostsQuery } from '../containers/kpi_hosts'; +import { useGlobalTime } from '../../common/containers/use_global_time'; import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; @@ -44,17 +45,8 @@ import { HostsTableType } from '../store/model'; const KpiHostsComponentManage = manageQuery(KpiHostsComponent); export const HostsComponent = React.memo( - ({ - deleteQuery, - isInitializing, - filters, - from, - query, - setAbsoluteRangeDatePicker, - setQuery, - to, - hostsPagePath, - }) => { + ({ filters, query, setAbsoluteRangeDatePicker, hostsPagePath }) => { + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); const capabilities = useMlCapabilities(); const kibana = useKibana(); const { tabName } = useParams(); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index c2285cf0a97e..75cd36924dbb 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -11,7 +11,6 @@ import { HostDetails } from './details'; import { HostsTableType } from '../store/model'; import { MlHostConditionalContainer } from '../../common/components/ml/conditional_links/ml_host_conditional_container'; -import { GlobalTime } from '../../common/containers/global_time'; import { Hosts } from './hosts'; import { hostsPagePath, hostDetailsPagePath } from './types'; @@ -36,72 +35,49 @@ type Props = Partial> & { url: string }; export const HostsContainer = React.memo(({ url }) => { const history = useHistory(); + return ( - - {({ to, from, setQuery, deleteQuery, isInitializing }) => ( - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - { - history.replace(`${detailName}/${HostsTableType.authentications}${search}`); - return null; - }} - /> + + ( + + )} + /> + + + + } + /> + { + history.replace(`${detailName}/${HostsTableType.authentications}${search}`); + return null; + }} + /> - { - history.replace(`${HostsTableType.hosts}${search}`); - return null; - }} - /> - - )} - + { + history.replace(`${HostsTableType.hosts}${search}`); + return null; + }} + /> + ); }); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts index 76f56fe1718a..ddee940d1179 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts @@ -7,8 +7,7 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { NarrowDateRange } from '../../../common/components/ml/types'; -import { InspectQuery, Refetch } from '../../../common/store/inputs/model'; - +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { HostsTableType, HostsType } from '../../store/model'; import { NavTab } from '../../../common/components/navigation/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; @@ -24,31 +23,19 @@ type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPe export type HostsNavTab = Record; -export type SetQuery = ({ - id, - inspect, - loading, - refetch, -}: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; -}) => void; - export interface QueryTabBodyProps { type: HostsType; - startDate: number; - endDate: number; + startDate: GlobalTimeArgs['from']; + endDate: GlobalTimeArgs['to']; filterQuery?: string | ESTermQuery; } export type HostsComponentsQueryProps = QueryTabBodyProps & { - deleteQuery?: ({ id }: { id: string }) => void; + deleteQuery?: GlobalTimeArgs['deleteQuery']; indexPattern: IIndexPattern; pageFilters?: Filter[]; skip: boolean; - setQuery: SetQuery; + setQuery: GlobalTimeArgs['setQuery']; updateDateRange?: UpdateDateRange; narrowDateRange?: NarrowDateRange; }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/types.ts index ffd17b0ef46f..2c9ca4e4d27d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/types.ts @@ -8,23 +8,26 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { ActionCreator } from 'typescript-fsa'; import { hostsModel } from '../store'; -import { GlobalTimeArgs } from '../../common/containers/global_time'; +import { GlobalTimeArgs } from '../../common/containers/use_global_time'; import { InputsModelId } from '../../common/store/inputs/constants'; export const hostsPagePath = '/'; export const hostDetailsPagePath = `/:detailName`; -export type HostsTabsProps = HostsComponentProps & { - filterQuery: string; - type: hostsModel.HostsType; - indexPattern: IIndexPattern; - setAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; -}; +export type HostsTabsProps = HostsComponentProps & + GlobalTimeArgs & { + filterQuery: string; + type: hostsModel.HostsType; + indexPattern: IIndexPattern; + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + }; export type HostsQueryProps = GlobalTimeArgs; -export type HostsComponentProps = HostsQueryProps & { hostsPagePath: string }; +export interface HostsComponentProps { + hostsPagePath: string; +} 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 77d4d4364acd..23ac6cc5b813 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/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 08c6ec89ff05..447a70ef998a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -92,6 +92,7 @@ export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['ite } isOpen={isOpen} closePopover={handleCloseMenu} + repositionOnScroll > diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx index 33eadad9aa77..76e197063fb8 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; import { EmbeddedMapComponent } from './embedded_map'; -import { SetQuery } from './types'; const mockUseIndexPatterns = useIndexPatterns as jest.Mock; jest.mock('../../../common/hooks/use_index_patterns'); @@ -18,7 +17,7 @@ mockUseIndexPatterns.mockImplementation(() => [true, []]); jest.mock('../../../common/lib/kibana'); describe('EmbeddedMapComponent', () => { - let setQuery: SetQuery; + let setQuery: jest.Mock; beforeEach(() => { setQuery = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 6470fc270d0b..81aa4b1671fc 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -15,13 +15,13 @@ import { getIndexPatternTitleIdMapping } from '../../../common/hooks/api/helpers import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; import { Loader } from '../../../common/components/loader'; import { displayErrorToast, useStateToaster } from '../../../common/components/toasters'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { Embeddable } from './embeddable'; import { EmbeddableHeader } from './embeddable_header'; import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; -import { SetQuery } from './types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MapEmbeddable } from '../../../../../../plugins/maps/public/embeddable'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; @@ -73,7 +73,7 @@ export interface EmbeddedMapProps { filters: Filter[]; startDate: number; endDate: number; - setQuery: SetQuery; + setQuery: GlobalTimeArgs['setQuery']; } export const EmbeddedMapComponent = ({ diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index b0f8e2cc0240..c58e53d07acb 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -8,7 +8,7 @@ import uuid from 'uuid'; import React from 'react'; import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; -import { IndexPatternMapping, SetQuery } from './types'; +import { IndexPatternMapping } from './types'; import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/public'; import { @@ -30,6 +30,7 @@ import { ErrorEmbeddable, } from '../../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../../common/hooks/types'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; /** * Creates MapEmbeddable with provided initial configuration @@ -49,9 +50,9 @@ export const createEmbeddable = async ( filters: Filter[], indexPatterns: IndexPatternMapping[], query: Query, - startDate: number, - endDate: number, - setQuery: SetQuery, + startDate: GlobalTimeArgs['from'], + endDate: GlobalTimeArgs['to'], + setQuery: GlobalTimeArgs['setQuery'], portalNode: PortalNode, embeddableApi: EmbeddableStart ): Promise => { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts index e3ca3c5b8428..700071f88a4b 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts @@ -6,7 +6,6 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { RenderTooltipContentParams } from '../../../../../maps/public/classes/tooltips/tooltip_property'; -import { inputsModel } from '../../../common/store/inputs'; export interface IndexPatternMapping { title: string; @@ -29,12 +28,10 @@ export interface LayerMappingCollection { [indexPatternTitle: string]: LayerMapping; } -export type SetQuery = (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; -}) => void; +export interface MapFeature { + id: number; + layerId: string; +} export interface FeatureGeometry { coordinates: [number]; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 2bae19ce89ae..72e3161de537 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -36,7 +36,7 @@ import { GetSubTitle, } from '../../../common/components/matrix_histogram/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; -import { SetQuery } from '../../../hosts/pages/navigation/types'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { networkModel, networkSelectors } from '../../store'; const ID = 'networkDnsQuery'; @@ -67,7 +67,7 @@ interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { isDnsHistogram?: boolean; query: DocumentNode; scaleType: ScaleType; - setQuery: SetQuery; + setQuery: GlobalTimeArgs['setQuery']; showLegend?: boolean; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index c7a8a5f705df..9ac05cc98bb4 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -13,7 +13,6 @@ import { FlowTarget } from '../../graphql/types'; import { IPDetails } from './ip_details'; import { Network } from './network'; -import { GlobalTime } from '../../common/containers/global_time'; import { getNetworkRoutePath } from './navigation'; import { NetworkRouteType } from './navigation/types'; import { MlNetworkConditionalContainer } from '../../common/components/ml/conditional_links/ml_network_conditional_container'; @@ -36,71 +35,48 @@ const NetworkContainerComponent: React.FC = () => { ); return ( - - {({ to, from, setQuery, deleteQuery, isInitializing }) => ( - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - { - history.replace(`ip/${detailName}/${FlowTarget.source}${search}`); - return null; - }} - /> - { - history.replace(`${NetworkRouteType.flows}${search}`); - return null; - }} - /> - - )} - + + ( + + )} + /> + + + + } + /> + { + history.replace(`ip/${detailName}/${FlowTarget.source}${search}`); + return null; + }} + /> + { + history.replace(`${NetworkRouteType.flows}${search}`); + return null; + }} + /> + ); }; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx index 962a6269f848..92f39228f07a 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx @@ -33,6 +33,11 @@ type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/containers/source'); +jest.mock('../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest + .fn() + .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), +})); // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index 162b3a7c158d..6686b40e3c42 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; import { FiltersGlobal } from '../../../common/components/filters_global'; import { HeaderPage } from '../../../common/components/header_page'; import { LastEventTime } from '../../../common/components/last_event_time'; @@ -51,14 +52,11 @@ export const IPDetailsComponent: React.FC { + const { to, from, setQuery, isInitializing } = useGlobalTime(); const type = networkModel.NetworkType.details; const narrowDateRange = useCallback( (score, interval) => { diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts index 02d83208884b..75fb5007f270 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts @@ -8,16 +8,15 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { ESTermQuery } from '../../../../common/typed_json'; import { NetworkType } from '../../store/model'; -import { InspectQuery, Refetch } from '../../../common/store/inputs/model'; import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; -import { GlobalTimeArgs } from '../../../common/containers/global_time'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; export const type = NetworkType.details; -export type IPDetailsComponentProps = GlobalTimeArgs & { +export interface IPDetailsComponentProps { detailName: string; flowTarget: FlowTarget; -}; +} export interface OwnProps { type: NetworkType; @@ -26,17 +25,7 @@ export interface OwnProps { filterQuery: string | ESTermQuery; ip: string; skip: boolean; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; - }) => void; + setQuery: GlobalTimeArgs['setQuery']; } export type NetworkComponentsQueryProps = OwnProps & { diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 433ed7fffd74..6986d10ad352 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -10,7 +10,7 @@ import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { NavTab } from '../../../common/components/navigation/types'; import { FlowTargetSourceDest } from '../../../graphql/types'; import { networkModel } from '../../store'; -import { GlobalTimeArgs } from '../../../common/containers/global_time'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SetAbsoluteRangeDatePicker } from '../types'; import { NarrowDateRange } from '../../../common/components/ml/types'; diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 4275c1641f51..bdaac1ac049e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -23,6 +23,7 @@ import { KpiNetworkComponent } from '..//components/kpi_network'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { KpiNetworkQuery } from '../../network/containers/kpi_network'; +import { useGlobalTime } from '../../common/containers/use_global_time'; import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; @@ -47,13 +48,10 @@ const NetworkComponent = React.memo( query, setAbsoluteRangeDatePicker, networkPagePath, - to, - from, - setQuery, - isInitializing, hasMlUserPermissions, capabilitiesFetched, }) => { + const { to, from, setQuery, isInitializing } = useGlobalTime(); const kibana = useKibana(); const { tabName } = useParams(); diff --git a/x-pack/plugins/security_solution/public/network/pages/types.ts b/x-pack/plugins/security_solution/public/network/pages/types.ts index e4170ee4b908..54ff5a8d50b8 100644 --- a/x-pack/plugins/security_solution/public/network/pages/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/types.ts @@ -7,7 +7,6 @@ import { RouteComponentProps } from 'react-router-dom'; import { ActionCreator } from 'typescript-fsa'; import { InputsModelId } from '../../common/store/inputs/constants'; -import { GlobalTimeArgs } from '../../common/containers/global_time'; export type SetAbsoluteRangeDatePicker = ActionCreator<{ id: InputsModelId; @@ -15,9 +14,8 @@ export type SetAbsoluteRangeDatePicker = ActionCreator<{ to: number; }>; -export type NetworkComponentProps = Partial> & - GlobalTimeArgs & { - networkPagePath: string; - hasMlUserPermissions: boolean; - capabilitiesFetched: boolean; - }; +export type NetworkComponentProps = Partial> & { + networkPagePath: string; + hasMlUserPermissions: boolean; + capabilitiesFetched: boolean; +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 03e8279f01db..6e59d81a1eae 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -19,7 +19,6 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../common/store'; import { HostsTableType, HostsType } from '../../../hosts/store/model'; import * as i18n from '../../pages/translations'; @@ -29,6 +28,7 @@ import { } from '../../../common/components/alerts_viewer/histogram_configs'; import { MatrixHisrogramConfigs } from '../../../common/components/matrix_histogram/types'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { useFormatUrl } from '../../../common/components/link_to'; import { LinkButton } from '../../../common/components/links'; @@ -39,20 +39,11 @@ const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const DEFAULT_STACK_BY = 'event.module'; -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; +interface Props extends Pick { filters?: Filter[]; - from: number; hideHeaderChildren?: boolean; indexPattern: IIndexPattern; query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; } const AlertsByCategoryComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx index 1773af86a382..23f5998f4411 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx @@ -20,7 +20,7 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../common/store'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; const HorizontalSpacer = styled(EuiFlexItem)` width: 24px; @@ -29,18 +29,10 @@ const HorizontalSpacer = styled(EuiFlexItem)` const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -interface Props { +interface Props extends Pick { filters?: Filter[]; - from: number; indexPattern: IIndexPattern; query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; } const EventCountsComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index fe3f9f8ecda3..f18fccee50e2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -27,9 +27,9 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../common/store'; import { HostsTableType, HostsType } from '../../../hosts/store/model'; import { InputsModelId } from '../../../common/store/inputs/constants'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import * as i18n from '../../pages/translations'; import { SecurityPageName } from '../../../app/types'; @@ -42,25 +42,17 @@ const DEFAULT_STACK_BY = 'event.dataset'; const ID = 'eventsByDatasetOverview'; -interface Props { +interface Props extends Pick { combinedQueries?: string; - deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; - from: number; headerChildren?: React.ReactNode; indexPattern: IIndexPattern; indexToAdd?: string[] | null; onlyField?: string; query?: Query; setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; showSpacer?: boolean; - to: number; + timelineId?: string; } const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ @@ -81,6 +73,7 @@ const EventsByDatasetComponent: React.FC = ({ setAbsoluteRangeDatePickerTarget, setQuery, showSpacer = true, + timelineId, to, }) => { // create a unique, but stable (across re-renders) query id @@ -177,6 +170,7 @@ const EventsByDatasetComponent: React.FC = ({ showSpacer={showSpacer} sourceId="default" startDate={from} + timelineId={timelineId} type={HostsType.page} {...eventsByDatasetHistogramConfigs} title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 195bb4fa0807..583c76d1464a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -18,26 +18,16 @@ import { useUiSetting$, useKibana } from '../../../common/lib/kibana'; import { getHostsUrl, useFormatUrl } from '../../../common/components/link_to'; import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { inputsModel } from '../../../common/store/inputs'; import { InspectButtonContainer } from '../../../common/components/inspect'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; export interface OwnProps { - startDate: number; - endDate: number; + startDate: GlobalTimeArgs['from']; + endDate: GlobalTimeArgs['to']; filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; + setQuery: GlobalTimeArgs['setQuery']; } const OverviewHostStatsManage = manageQuery(OverviewHostStats); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index a3760863bcb6..8282eaeb63c2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -19,28 +19,18 @@ import { ID as OverviewNetworkQueryId, OverviewNetworkQuery, } from '../../containers/overview_network'; -import { inputsModel } from '../../../common/store/inputs'; import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; import { getNetworkUrl, useFormatUrl } from '../../../common/components/link_to'; import { InspectButtonContainer } from '../../../common/components/inspect'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; export interface OverviewNetworkProps { - startDate: number; - endDate: number; + startDate: GlobalTimeArgs['from']; + endDate: GlobalTimeArgs['to']; filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; + setQuery: GlobalTimeArgs['setQuery']; } const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index 5010fd9c06eb..8b62df60b257 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -11,19 +11,17 @@ import { alertsHistogramOptions } from '../../../alerts/components/alerts_histog import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; import { SetAbsoluteRangeDatePicker } from '../../../network/pages/types'; import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../common/store'; import { InputsModelId } from '../../../common/store/inputs/constants'; import * as i18n from '../../pages/translations'; import { UpdateDateRange } from '../../../common/components/charts/common'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; const NO_FILTERS: Filter[] = []; -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; +interface Props extends Pick { filters?: Filter[]; - from: number; headerChildren?: React.ReactNode; indexPattern: IIndexPattern; /** Override all defaults, and only display this field */ @@ -31,13 +29,7 @@ interface Props { query?: Query; setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; + timelineId?: string; } const SignalsByCategoryComponent: React.FC = ({ @@ -50,6 +42,7 @@ const SignalsByCategoryComponent: React.FC = ({ setAbsoluteRangeDatePicker, setAbsoluteRangeDatePickerTarget = 'global', setQuery, + timelineId, to, }) => { const { signalIndexName } = useSignalIndex(); @@ -83,6 +76,7 @@ const SignalsByCategoryComponent: React.FC = ({ showLinkToAlerts={onlyField == null ? true : false} stackByOptions={onlyField == null ? alertsHistogramOptions : undefined} legendPosition={'right'} + timelineId={timelineId} to={to} title={i18n.ALERT_COUNT} updateDateRange={updateDateRangeCallback} diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index bf5e7f0c211b..9613a1e7210a 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -19,6 +19,11 @@ import { Overview } from './index'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); +jest.mock('../../common/containers/use_global_time', () => ({ + useGlobalTime: jest + .fn() + .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), +})); // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index b8b8a67024c9..2a522d3ea8fd 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { useState, useMemo } from 'react'; +import React, { useCallback, useState, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; import { Query, Filter } from 'src/plugins/data/public'; @@ -15,7 +15,7 @@ import { AlertsByCategory } from '../components/alerts_by_category'; import { FiltersGlobal } from '../../common/components/filters_global'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { GlobalTime } from '../../common/containers/global_time'; +import { useGlobalTime } from '../../common/containers/use_global_time'; import { useWithSource } from '../../common/containers/source'; import { EventsByDataset } from '../components/events_by_dataset'; import { EventCounts } from '../components/event_counts'; @@ -46,6 +46,7 @@ const OverviewComponent: React.FC = ({ return [ENDPOINT_METADATA_INDEX]; }, []); + const { from, deleteQuery, setQuery, to } = useGlobalTime(); const { indicesExist, indexPattern } = useWithSource(); const { indicesExist: metadataIndexExists } = useWithSource( 'default', @@ -59,10 +60,10 @@ const OverviewComponent: React.FC = ({ ); const [dismissMessage, setDismissMessage] = useState(hasDismissEndpointNoticeMessage); - const dismissEndpointNotice = () => { + const dismissEndpointNotice = useCallback(() => { setDismissMessage(true); addMessage('management', 'dismissEndpointNotice'); - }; + }, [addMessage]); return ( <> @@ -85,59 +86,55 @@ const OverviewComponent: React.FC = ({ - - {({ from, deleteQuery, setQuery, to }) => ( - - - - - - - - - - - - - - - - - - - )} - + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index d3bb6123ce04..ce126bf69555 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -215,6 +215,7 @@ const NodeSubMenuComponents = React.memo( button={submenuPopoverButton} isOpen={menuIsOpen} closePopover={closePopover} + repositionOnScroll > {menuIsOpen && typeof optionsWithActions === 'object' && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 7296e0ee4b97..80fe7cb33779 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -274,6 +274,7 @@ export const DefaultFieldRendererOverflow = React.memo setIsOpen(!isOpen)} + repositionOnScroll > diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index da0cbb99b867..1f917c664e81 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -24,7 +24,6 @@ const defaultProps = { }), fieldId: timestampFieldId, onUpdateColumns: jest.fn(), - timelineId: 'timeline-id', }; describe('FieldName', () => { @@ -46,8 +45,7 @@ describe('FieldName', () => { ); - - wrapper.simulate('mouseenter'); + wrapper.find('div').at(1).simulate('mouseenter'); wrapper.update(); expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx index 985c8b35094e..62e41d967cb9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx @@ -5,13 +5,16 @@ */ import { EuiHighlight, EuiText } from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; +import React, { useCallback, useState, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../timeline/events'; import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { DraggableWrapperHoverContent } from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; +import { + DraggableWrapperHoverContent, + useGetTimelineId, +} from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; /** * The name of a (draggable) field @@ -77,23 +80,34 @@ export const FieldName = React.memo<{ fieldId: string; highlight?: string; onUpdateColumns: OnUpdateColumns; - timelineId: string; -}>(({ fieldId, highlight = '', timelineId }) => { +}>(({ fieldId, highlight = '' }) => { + const containerRef = useRef(null); + const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); const [showTopN, setShowTopN] = useState(false); + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(containerRef, goGetTimelineId); + const toggleTopN = useCallback(() => { - setShowTopN(!showTopN); - }, [setShowTopN, showTopN]); + setShowTopN((prevShowTopN) => !prevShowTopN); + }, []); + + const handleClosePopOverTrigger = useCallback( + () => setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger), + [] + ); const hoverContent = useMemo( () => ( ), - [fieldId, showTopN, toggleTopN, timelineId] + [fieldId, handleClosePopOverTrigger, showTopN, timelineIdFind, toggleTopN] ); const render = useCallback( @@ -109,7 +123,16 @@ export const FieldName = React.memo<{ [fieldId, highlight] ); - return ; + return ( +
+ +
+ ); }); FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index be655f7465a1..3b40c36fccd1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -57,6 +57,11 @@ type ActionManageTimeline = id: string; payload: boolean; } + | { + type: 'SET_INDEX_TO_ADD'; + id: string; + payload: string[]; + } | { type: 'SET_TIMELINE_ACTIONS'; id: string; @@ -81,7 +86,10 @@ export const timelineDefaults = { title: i18n.EVENTS, unit: (n: number) => i18n.UNIT(n), }; -const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTimeline) => { +const reducerManageTimeline = ( + state: ManageTimelineById, + action: ActionManageTimeline +): ManageTimelineById => { switch (action.type) { case 'INITIALIZE_TIMELINE': return { @@ -91,7 +99,15 @@ const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTi ...state[action.id], ...action.payload, }, - }; + } as ManageTimelineById; + case 'SET_INDEX_TO_ADD': + return { + ...state, + [action.id]: { + ...state[action.id], + indexToAdd: action.payload, + }, + } as ManageTimelineById; case 'SET_TIMELINE_ACTIONS': case 'SET_TIMELINE_FILTER_MANAGER': return { @@ -100,7 +116,7 @@ const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTi ...state[action.id], ...action.payload, }, - }; + } as ManageTimelineById; case 'SET_IS_LOADING': return { ...state, @@ -108,7 +124,7 @@ const reducerManageTimeline = (state: ManageTimelineById, action: ActionManageTi ...state[action.id], isLoading: action.payload, }, - }; + } as ManageTimelineById; default: return state; } @@ -119,6 +135,7 @@ interface UseTimelineManager { getTimelineFilterManager: (id: string) => FilterManager | undefined; initializeTimeline: (newTimeline: ManageTimelineInit) => void; isManagedTimeline: (id: string) => boolean; + setIndexToAdd: (indexToAddArgs: { id: string; indexToAdd: string[] }) => void; setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; setTimelineRowActions: (actionsArgs: { id: string; @@ -129,10 +146,9 @@ interface UseTimelineManager { } const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseTimelineManager => { - const [state, dispatch] = useReducer( - reducerManageTimeline, - manageTimelineForTesting ?? initManageTimeline - ); + const [state, dispatch] = useReducer< + (state: ManageTimelineById, action: ActionManageTimeline) => ManageTimelineById + >(reducerManageTimeline, manageTimelineForTesting ?? initManageTimeline); const initializeTimeline = useCallback((newTimeline: ManageTimelineInit) => { dispatch({ @@ -183,8 +199,16 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT [] ); + const setIndexToAdd = useCallback(({ id, indexToAdd }: { id: string; indexToAdd: string[] }) => { + dispatch({ + type: 'SET_INDEX_TO_ADD', + id, + payload: indexToAdd, + }); + }, []); + const getTimelineFilterManager = useCallback( - (id: string): FilterManager | undefined => state[id].filterManager, + (id: string): FilterManager | undefined => state[id]?.filterManager, [state] ); const getManageTimelineById = useCallback( @@ -195,8 +219,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT initializeTimeline({ id }); return { ...timelineDefaults, id }; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [state] + [initializeTimeline, state] ); const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]); @@ -205,6 +228,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT getTimelineFilterManager, initializeTimeline, isManagedTimeline, + setIndexToAdd, setIsTimelineLoading, setTimelineRowActions, setTimelineFilterManager, @@ -214,6 +238,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT const init = { getManageTimelineById: (id: string) => ({ ...timelineDefaults, id }), getTimelineFilterManager: () => undefined, + setIndexToAdd: () => undefined, isManagedTimeline: () => false, initializeTimeline: () => noop, setIsTimelineLoading: () => noop, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 03a964bbd444..8855cba7a4c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -187,6 +187,7 @@ export const EventColumnView = React.memo( closePopover={closePopover} panelPaddingSize="none" anchorPosition="downLeft" + repositionOnScroll > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 51bf883ed2d6..43ea5e905ca8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -17,6 +17,7 @@ import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { wait } from '../../../../common/lib/helpers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; +import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../styles'; const testBodyHeight = 700; const mockGetNotesByIds = (eventId: string[]) => []; @@ -133,6 +134,20 @@ describe('Body', () => { ).toEqual(true); }); }, 20000); + + test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_BODY_CLASS_NAME}`, () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_BODY_CLASS_NAME}`) + .first() + .exists() + ).toEqual(true); + }); }); describe('action on event', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 46895c86de08..6a296170fffd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -139,6 +139,7 @@ export const Body = React.memo( )} ( pinnedEventIds={pinnedEventIds} rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]} selectedEventIds={selectedEventIds} - show={id === ACTIVE_TIMELINE_REDUX_ID ? show : true} + show={id === TimelineId.active ? show : true} showCheckboxes={showCheckboxes} sort={sort} toggleColumn={toggleColumn} 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 ef7ee26cd3ec..5af2f3ef488b 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/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx index 83417cdb51b6..0adf76730826 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx @@ -100,6 +100,7 @@ export const InsertTimelinePopoverComponent: React.FC = ({ button={insertTimelineButton} isOpen={isPopoverOpen} closePopover={handleClosePopover} + repositionOnScroll > = ({ id="timelineSettingsPopover" isOpen={showActions} closePopover={onClosePopover} + repositionOnScroll > {capabilitiesCanUserCRUD && ( 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 b27f213c6a02..47d848021ba4 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 @@ -14,16 +14,17 @@ import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/component /** * TIMELINE BODY */ +export const SELECTOR_TIMELINE_BODY_CLASS_NAME = 'securitySolutionTimeline__body'; // SIDE EFFECT: the following creates a global class selector export const TimelineBodyGlobalStyle = createGlobalStyle` - body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .siemTimeline__body { + body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .${SELECTOR_TIMELINE_BODY_CLASS_NAME} { overflow: hidden; } `; export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ - className: `siemTimeline__body ${className}`, + className: `${SELECTOR_TIMELINE_BODY_CLASS_NAME} ${className}`, }))<{ bodyHeight?: number; visible: boolean }>` height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; overflow: auto; @@ -204,6 +205,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/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index 07d4b004d2ed..18deaf015872 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -174,6 +174,7 @@ export const TimelineComponent: React.FC = ({ const [isQueryLoading, setIsQueryLoading] = useState(false); const { initializeTimeline, + setIndexToAdd, setIsTimelineLoading, setTimelineFilterManager, setTimelineRowActions, @@ -188,12 +189,14 @@ export const TimelineComponent: React.FC = ({ }, []); useEffect(() => { setIsTimelineLoading({ id, isLoading: isQueryLoading || loadingIndexName }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadingIndexName, isQueryLoading]); + }, [loadingIndexName, id, isQueryLoading, setIsTimelineLoading]); useEffect(() => { setTimelineFilterManager({ id, filterManager }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filterManager]); + }, [filterManager, id, setTimelineFilterManager]); + + useEffect(() => { + setIndexToAdd({ id, indexToAdd }); + }, [id, indexToAdd, setIndexToAdd]); return ( 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 344d0f0e5131..5c31b3fad685 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/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index 67a331f4ba67..ace5aec77ed2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -57,6 +57,8 @@ export const getPackageConfigCreateCallback = ( try { return updatedPackageConfig; } finally { + // TODO: confirm creation of package config + // then commit. await manifestManager.commit(wrappedManifest); } }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 4c3153ca0ef1..b6a5bed9078a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -6,12 +6,12 @@ export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', - SAVED_OBJECT_TYPE: 'endpoint:exceptions-artifact', + SAVED_OBJECT_TYPE: 'endpoint:user-artifact', SUPPORTED_OPERATING_SYSTEMS: ['linux', 'macos', 'windows'], SCHEMA_VERSION: '1.0.0', }; export const ManifestConstants = { - SAVED_OBJECT_TYPE: 'endpoint:exceptions-manifest', + SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest', SCHEMA_VERSION: '1.0.0', }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 7fd057afdbd5..2abb72234fec 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -7,22 +7,20 @@ import { createHash } from 'crypto'; import { validate } from '../../../../common/validate'; -import { - Entry, - EntryNested, - EntryMatch, - EntryMatchAny, -} from '../../../../../lists/common/schemas/types/entries'; +import { Entry, EntryNested } from '../../../../../lists/common/schemas/types/entries'; import { FoundExceptionListItemSchema } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema'; import { ExceptionListClient } from '../../../../../lists/server'; import { InternalArtifactSchema, TranslatedEntry, - TranslatedEntryMatch, - TranslatedEntryMatchAny, - TranslatedEntryNested, WrappedTranslatedExceptionList, wrappedExceptionList, + TranslatedEntryNestedEntry, + translatedEntryNestedEntry, + translatedEntry as translatedEntryType, + TranslatedEntryMatcher, + translatedEntryMatchMatcher, + translatedEntryMatchAnyMatcher, } from '../../schemas'; import { ArtifactConstants } from './common'; @@ -36,11 +34,14 @@ export async function buildArtifact( return { identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`, - sha256, - encoding: 'application/json', + compressionAlgorithm: 'none', + encryptionAlgorithm: 'none', + decompressedSha256: sha256, + compressedSha256: sha256, + decompressedSize: exceptionsBuffer.byteLength, + compressedSize: exceptionsBuffer.byteLength, created: Date.now(), body: exceptionsBuffer.toString('base64'), - size: exceptionsBuffer.byteLength, }; } @@ -92,66 +93,80 @@ export function translateToEndpointExceptions( exc: FoundExceptionListItemSchema, schemaVersion: string ): TranslatedEntry[] { - const translatedList: TranslatedEntry[] = []; - if (schemaVersion === '1.0.0') { - exc.data.forEach((list) => { - list.entries.forEach((entry) => { - const tEntry = translateEntry(schemaVersion, entry); - if (tEntry !== undefined) { - translatedList.push(tEntry); + return exc.data + .flatMap((list) => { + return list.entries; + }) + .reduce((entries: TranslatedEntry[], entry) => { + const translatedEntry = translateEntry(schemaVersion, entry); + if (translatedEntry !== undefined && translatedEntryType.is(translatedEntry)) { + entries.push(translatedEntry); } - }); - }); + return entries; + }, []); } else { throw new Error('unsupported schemaVersion'); } - return translatedList; +} + +function getMatcherFunction(field: string, matchAny?: boolean): TranslatedEntryMatcher { + return matchAny + ? field.endsWith('.text') + ? 'exact_caseless_any' + : 'exact_cased_any' + : field.endsWith('.text') + ? 'exact_caseless' + : 'exact_cased'; +} + +function normalizeFieldName(field: string): string { + return field.endsWith('.text') ? field.substring(0, field.length - 5) : field; } function translateEntry( schemaVersion: string, entry: Entry | EntryNested ): TranslatedEntry | undefined { - let translatedEntry; switch (entry.type) { case 'nested': { - const e = (entry as unknown) as EntryNested; - const nestedEntries: TranslatedEntry[] = []; - for (const nestedEntry of e.entries) { - const translation = translateEntry(schemaVersion, nestedEntry); - if (translation !== undefined) { - nestedEntries.push(translation); - } - } - translatedEntry = { + const nestedEntries = entry.entries.reduce( + (entries: TranslatedEntryNestedEntry[], nestedEntry) => { + const translatedEntry = translateEntry(schemaVersion, nestedEntry); + if (nestedEntry !== undefined && translatedEntryNestedEntry.is(translatedEntry)) { + entries.push(translatedEntry); + } + return entries; + }, + [] + ); + return { entries: nestedEntries, - field: e.field, + field: entry.field, type: 'nested', - } as TranslatedEntryNested; - break; + }; } case 'match': { - const e = (entry as unknown) as EntryMatch; - translatedEntry = { - field: e.field.endsWith('.text') ? e.field.substring(0, e.field.length - 5) : e.field, - operator: e.operator, - type: e.field.endsWith('.text') ? 'exact_caseless' : 'exact_cased', - value: e.value, - } as TranslatedEntryMatch; - break; + const matcher = getMatcherFunction(entry.field); + return translatedEntryMatchMatcher.is(matcher) + ? { + field: normalizeFieldName(entry.field), + operator: entry.operator, + type: matcher, + value: entry.value, + } + : undefined; + } + case 'match_any': { + const matcher = getMatcherFunction(entry.field, true); + return translatedEntryMatchAnyMatcher.is(matcher) + ? { + field: normalizeFieldName(entry.field), + operator: entry.operator, + type: matcher, + value: entry.value, + } + : undefined; } - case 'match_any': - { - const e = (entry as unknown) as EntryMatchAny; - translatedEntry = { - field: e.field.endsWith('.text') ? e.field.substring(0, e.field.length - 5) : e.field, - operator: e.operator, - type: e.field.endsWith('.text') ? 'exact_caseless_any' : 'exact_cased_any', - value: e.value, - } as TranslatedEntryMatchAny; - } - break; } - return translatedEntry || undefined; } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index 0434e3d8ffcb..da8a449e1b02 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -55,21 +55,33 @@ describe('manifest', () => { expect(manifest1.toEndpointFormat()).toStrictEqual({ artifacts: { 'endpoint-exceptionlist-linux-1.0.0': { - sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', - size: 268, - url: + compression_algorithm: 'none', + encryption_algorithm: 'none', + precompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + postcompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + precompress_size: 268, + postcompress_size: 268, + relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', }, 'endpoint-exceptionlist-macos-1.0.0': { - sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', - size: 268, - url: + compression_algorithm: 'none', + encryption_algorithm: 'none', + precompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + postcompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + precompress_size: 268, + postcompress_size: 268, + relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', }, 'endpoint-exceptionlist-windows-1.0.0': { - sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', - size: 268, - url: + compression_algorithm: 'none', + encryption_algorithm: 'none', + precompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + postcompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + precompress_size: 268, + postcompress_size: 268, + relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', }, }, @@ -107,7 +119,7 @@ describe('manifest', () => { test('Manifest returns data for given artifact', async () => { const artifact = artifacts[0]; - const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.sha256}`); + const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.compressedSha256}`); expect(returned).toEqual(artifact); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts index 34bd2b0f388e..c8cbdfc2fc5f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts @@ -33,13 +33,17 @@ describe('manifest_entry', () => { }); test('Correct sha256 is returned', () => { - expect(manifestEntry.getSha256()).toEqual( + expect(manifestEntry.getCompressedSha256()).toEqual( + '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ); + expect(manifestEntry.getDecompressedSha256()).toEqual( '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' ); }); test('Correct size is returned', () => { - expect(manifestEntry.getSize()).toEqual(268); + expect(manifestEntry.getCompressedSize()).toEqual(268); + expect(manifestEntry.getDecompressedSize()).toEqual(268); }); test('Correct url is returned', () => { @@ -54,9 +58,13 @@ describe('manifest_entry', () => { test('Correct record is returned', () => { expect(manifestEntry.getRecord()).toEqual({ - sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', - size: 268, - url: + compression_algorithm: 'none', + encryption_algorithm: 'none', + precompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + postcompress_sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + precompress_size: 268, + postcompress_size: 268, + relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts index 00fd446bf14b..860c2d7d704b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts @@ -15,23 +15,31 @@ export class ManifestEntry { } public getDocId(): string { - return `${this.getIdentifier()}-${this.getSha256()}`; + return `${this.getIdentifier()}-${this.getCompressedSha256()}`; } public getIdentifier(): string { return this.artifact.identifier; } - public getSha256(): string { - return this.artifact.sha256; + public getCompressedSha256(): string { + return this.artifact.compressedSha256; } - public getSize(): number { - return this.artifact.size; + public getDecompressedSha256(): string { + return this.artifact.decompressedSha256; + } + + public getCompressedSize(): number { + return this.artifact.compressedSize; + } + + public getDecompressedSize(): number { + return this.artifact.decompressedSize; } public getUrl(): string { - return `/api/endpoint/artifacts/download/${this.getIdentifier()}/${this.getSha256()}`; + return `/api/endpoint/artifacts/download/${this.getIdentifier()}/${this.getCompressedSha256()}`; } public getArtifact(): InternalArtifactSchema { @@ -40,9 +48,13 @@ export class ManifestEntry { public getRecord(): ManifestEntrySchema { return { - sha256: this.getSha256(), - size: this.getSize(), - url: this.getUrl(), + compression_algorithm: 'none', + encryption_algorithm: 'none', + precompress_sha256: this.getDecompressedSha256(), + precompress_size: this.getDecompressedSize(), + postcompress_sha256: this.getCompressedSha256(), + postcompress_size: this.getCompressedSize(), + relative_url: this.getUrl(), }; } } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts index d38026fbcbbd..5e61b278e87e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts @@ -16,11 +16,27 @@ export const exceptionsArtifactSavedObjectMappings: SavedObjectsType['mappings'] identifier: { type: 'keyword', }, - sha256: { + compressionAlgorithm: { type: 'keyword', + index: false, + }, + encryptionAlgorithm: { + type: 'keyword', + index: false, }, - encoding: { + compressedSha256: { type: 'keyword', + }, + compressedSize: { + type: 'long', + index: false, + }, + decompressedSha256: { + type: 'keyword', + index: false, + }, + decompressedSize: { + type: 'long', index: false, }, created: { @@ -31,10 +47,6 @@ export const exceptionsArtifactSavedObjectMappings: SavedObjectsType['mappings'] type: 'binary', index: false, }, - size: { - type: 'long', - index: false, - }, }, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index 08d02e70dac1..78b60e9e61f3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -14,7 +14,7 @@ import { EndpointAppContext } from '../../types'; export const ManifestTaskConstants = { TIMEOUT: '1m', - TYPE: 'securitySolution:endpoint:exceptions-packager', + TYPE: 'endpoint:user-artifact-packager', VERSION: '1.0.0', }; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts index 21d1105a313e..d071896c537b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts @@ -20,6 +20,9 @@ export const translatedEntryMatchAny = t.exact( ); export type TranslatedEntryMatchAny = t.TypeOf; +export const translatedEntryMatchAnyMatcher = translatedEntryMatchAny.type.props.type; +export type TranslatedEntryMatchAnyMatcher = t.TypeOf; + export const translatedEntryMatch = t.exact( t.type({ field: t.string, @@ -33,11 +36,23 @@ export const translatedEntryMatch = t.exact( ); export type TranslatedEntryMatch = t.TypeOf; +export const translatedEntryMatchMatcher = translatedEntryMatch.type.props.type; +export type TranslatedEntryMatchMatcher = t.TypeOf; + +export const translatedEntryMatcher = t.union([ + translatedEntryMatchMatcher, + translatedEntryMatchAnyMatcher, +]); +export type TranslatedEntryMatcher = t.TypeOf; + +export const translatedEntryNestedEntry = t.union([translatedEntryMatch, translatedEntryMatchAny]); +export type TranslatedEntryNestedEntry = t.TypeOf; + export const translatedEntryNested = t.exact( t.type({ field: t.string, type: t.keyof({ nested: null }), - entries: t.array(t.union([translatedEntryMatch, translatedEntryMatchAny])), + entries: t.array(translatedEntryNestedEntry), }) ); export type TranslatedEntryNested = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts index 2e71ef98387f..fe032586dda5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts @@ -5,17 +5,26 @@ */ import * as t from 'io-ts'; -import { identifier, sha256, size } from '../../../../common/endpoint/schema/common'; -import { body, created, encoding } from './common'; +import { + compressionAlgorithm, + encryptionAlgorithm, + identifier, + sha256, + size, +} from '../../../../common/endpoint/schema/common'; +import { body, created } from './common'; export const internalArtifactSchema = t.exact( t.type({ identifier, - sha256, - encoding, + compressionAlgorithm, + encryptionAlgorithm, + decompressedSha256: sha256, + decompressedSize: size, + compressedSha256: sha256, + compressedSize: size, created, body, - size, }) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts index 4a3dcaae1bd3..00ae802ba6f3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts @@ -16,7 +16,7 @@ export class ArtifactClient { } public getArtifactId(artifact: InternalArtifactSchema) { - return `${artifact.identifier}-${artifact.sha256}`; + return `${artifact.identifier}-${artifact.compressedSha256}`; } public async getArtifact(id: string): Promise> { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index bbb6fdfd5081..ef4f921cb537 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -38,9 +38,13 @@ describe('manifest_manager', () => { schema_version: '1.0.0', artifacts: { [artifact.identifier]: { - sha256: artifact.sha256, - size: artifact.size, - url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.sha256}`, + compression_algorithm: 'none', + encryption_algorithm: 'none', + precompress_sha256: artifact.decompressedSha256, + postcompress_sha256: artifact.compressedSha256, + precompress_size: artifact.decompressedSize, + postcompress_size: artifact.compressedSize, + relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.compressedSha256}`, }, }, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 33b0d5db575c..e47a23b893b7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -180,17 +180,15 @@ export class ManifestManager { this.logger.info(`Dispatching new manifest with diffs: ${showDiffs(wrappedManifest.diffs)}`); let paging = true; + let page = 1; let success = true; while (paging) { - const { items, total, page } = await this.packageConfigService.list( - this.savedObjectsClient, - { - page: 1, - perPage: 20, - kuery: 'ingest-package-configs.package.name:endpoint', - } - ); + const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, { + page, + perPage: 20, + kuery: 'ingest-package-configs.package.name:endpoint', + }); for (const packageConfig of items) { const { id, revision, updated_at, updated_by, ...newPackageConfig } = packageConfig; @@ -222,6 +220,7 @@ export class ManifestManager { } paging = page * items.length < total; + page++; } return success ? wrappedManifest : null; diff --git a/x-pack/plugins/snapshot_restore/README.md b/x-pack/plugins/snapshot_restore/README.md new file mode 100644 index 000000000000..e11483785e95 --- /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 b680f4d3ebf9..3e16e3b5301e 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 739c72fe03a6..3b18af7cebbf 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/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index 1e01e04332f4..babd25dd3ec4 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -43,7 +43,7 @@ const features = ([ id: 'feature_3', name: 'Feature 3', navLinkId: 'feature3', - app: [], + app: ['feature3_app'], catalogue: ['feature3Entry'], management: { kibana: ['indices'], @@ -67,6 +67,7 @@ const buildCapabilities = () => feature1: true, feature2: true, feature3: true, + feature3_app: true, unknownFeature: true, }, catalogue: { @@ -241,6 +242,7 @@ describe('capabilitiesSwitcher', () => { expectedCapabilities.feature_2.foo = false; expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.navLinks.feature3_app = false; expectedCapabilities.catalogue.feature3Entry = false; expectedCapabilities.management.kibana.indices = false; expectedCapabilities.feature_3.bar = false; diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index a0cdd5ad0e93..05d042959648 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -68,6 +68,12 @@ function toggleDisabledFeatures( navLinks[feature.navLinkId] = false; } + feature.app.forEach((app) => { + if (navLinks.hasOwnProperty(app)) { + navLinks[app] = false; + } + }); + // Disable associated catalogue entries const privilegeCatalogueEntries = feature.catalogue || []; privilegeCatalogueEntries.forEach((catalogueEntryId) => { 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 4c9f62503a21..87c2fee4ea9b 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 eac9d09685a4..07a9076359f0 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 17292adad3eb..92374908c60f 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 7a9fa0c45e15..4c690a5675f6 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 fec72c317225..771b4e2d7d9c 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 c63f4ac72ed2..4a691e17011e 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 beb5ee6be67e..cc5cc87c754c 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 4c1572ddfcad..51c6b33579f5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7316,8 +7316,6 @@ "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", "xpack.infra.alerting.manageAlerts": "アラートを管理", "xpack.infra.analysisSetup.actionStepTitle": "MLジョブを作成", - "xpack.infra.analysisSetup.analysisSetupDescription": "機械学習を使用して自動的に異常ログレートカウントを検出します。", - "xpack.infra.analysisSetup.analysisSetupTitle": "機械学習分析を有効にする", "xpack.infra.analysisSetup.configurationStepTitle": "構成", "xpack.infra.analysisSetup.createMlJobButton": "ML ジョブを作成", "xpack.infra.analysisSetup.deleteAnalysisResultsWarning": "これにより以前検出された異常が削除されます。", @@ -7473,12 +7471,9 @@ "xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription": "指定された条件と一致したログエントリ数", "xpack.infra.logs.alerting.threshold.fired": "実行", "xpack.infra.logs.analysis.analyzeInMlButtonLabel": "ML で分析", - "xpack.infra.logs.analysis.anomaliesExpandedRowNumberOfLogEntriesDescription": "ログエントリーの数です", "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "15 分ごとのログエントリー (平均)", "xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "異常を読み込み中", "xpack.infra.logs.analysis.anomaliesSectionTitle": "異常", - "xpack.infra.logs.analysis.anomaliesTableMaxAnomalyScoreColumnName": "最高異常スコア", - "xpack.infra.logs.analysis.anomaliesTablePartitionColumnName": "パーティション", "xpack.infra.logs.analysis.anomalySectionNoAnomaliesTitle": "異常が検出されませんでした。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。", @@ -7505,9 +7500,6 @@ "xpack.infra.logs.analysis.mlUnavailableTitle": "この機能には機械学習が必要です", "xpack.infra.logs.analysis.onboardingSuccessContent": "機械学習ロボットがデータの収集を開始するまでしばらくお待ちください。", "xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!", - "xpack.infra.logs.analysis.overallAnomaliesNumberOfLogEntriesDescription": "ログエントリーの数です", - "xpack.infra.logs.analysis.overallAnomaliesTopAnomalyScoreDescription": "最高異常スコア", - "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最高異常スコア", "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最高異常スコア: {maxAnomalyScore}", "xpack.infra.logs.analysis.recreateJobButtonLabel": "ML ジョブを再作成", "xpack.infra.logs.analysis.setupStatusTryAgainButton": "再試行", @@ -7552,10 +7544,6 @@ "xpack.infra.logs.logEntryCategories.countColumnTitle": "メッセージ数", "xpack.infra.logs.logEntryCategories.datasetColumnTitle": "データセット", "xpack.infra.logs.logEntryCategories.datasetFilterPlaceholder": "データセットでフィルター", - "xpack.infra.logs.logEntryCategories.exampleEmptyDescription": "選択した時間範囲内に例は見つかりませんでした。ログエントリー保持期間を長くするとメッセージサンプルの可用性が向上します。", - "xpack.infra.logs.logEntryCategories.exampleEmptyReloadButtonLabel": "再読み込み", - "xpack.infra.logs.logEntryCategories.exampleLoadingFailureDescription": "カテゴリーの例を読み込めませんでした。", - "xpack.infra.logs.logEntryCategories.exampleLoadingFailureRetryButtonLabel": "再試行", "xpack.infra.logs.logEntryCategories.jobStatusLoadingMessage": "分類ジョブのステータスを確認中...", "xpack.infra.logs.logEntryCategories.loadDataErrorTitle": "カテゴリーデータを読み込めませんでした", "xpack.infra.logs.logEntryCategories.manyCategoriesWarningReasonDescription": "分析されたドキュメントごとのカテゴリ比率が{categoriesDocumentRatio, number }で、非常に高い値です。", @@ -8469,13 +8457,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": "保存中…", @@ -9790,8 +9772,6 @@ "xpack.ml.explorer.jobIdLabel": "ジョブ ID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)", "xpack.ml.explorer.kueryBar.filterPlaceholder": "影響因子フィールドでフィルタリング… ({queryExample})", - "xpack.ml.explorer.limitLabel": "制限", - "xpack.ml.explorer.loadingLabel": "読み込み中", "xpack.ml.explorer.noConfiguredInfluencersTooltip": "選択されたジョブに影響因子が構成されていないため、トップインフルエンスリストは非表示になっています。", "xpack.ml.explorer.noInfluencersFoundTitle": "{viewBySwimlaneFieldName}影響因子が見つかりません", "xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "指定されたフィルターの{viewBySwimlaneFieldName} 影響因子が見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 97f10e77dc71..8121df6d0509 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7320,8 +7320,6 @@ "xpack.infra.alerting.logs.manageAlerts": "管理告警", "xpack.infra.alerting.manageAlerts": "管理告警", "xpack.infra.analysisSetup.actionStepTitle": "创建 ML 作业", - "xpack.infra.analysisSetup.analysisSetupDescription": "使用 Machine Learning 自动检测异常日志速率计数。", - "xpack.infra.analysisSetup.analysisSetupTitle": "启用 Machine Learning 分析", "xpack.infra.analysisSetup.configurationStepTitle": "配置", "xpack.infra.analysisSetup.createMlJobButton": "创建 ML 作业", "xpack.infra.analysisSetup.deleteAnalysisResultsWarning": "这将移除以前检测到的异常。", @@ -7477,12 +7475,9 @@ "xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription": "匹配所提供条件的日志条目数", "xpack.infra.logs.alerting.threshold.fired": "已触发", "xpack.infra.logs.analysis.analyzeInMlButtonLabel": "在 ML 中分析", - "xpack.infra.logs.analysis.anomaliesExpandedRowNumberOfLogEntriesDescription": "日志条目数", "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "每 15 分钟日志条目数(平均值)", "xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "正在加载异常", "xpack.infra.logs.analysis.anomaliesSectionTitle": "异常", - "xpack.infra.logs.analysis.anomaliesTableMaxAnomalyScoreColumnName": "最大异常分数", - "xpack.infra.logs.analysis.anomaliesTablePartitionColumnName": "分区", "xpack.infra.logs.analysis.anomalySectionNoAnomaliesTitle": "未检测到任何异常。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。", @@ -7509,9 +7504,6 @@ "xpack.infra.logs.analysis.mlUnavailableTitle": "此功能需要 Machine Learning", "xpack.infra.logs.analysis.onboardingSuccessContent": "请注意,我们的 Machine Learning 机器人若干分钟后才会开始收集数据。", "xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!", - "xpack.infra.logs.analysis.overallAnomaliesNumberOfLogEntriesDescription": "日志条目数", - "xpack.infra.logs.analysis.overallAnomaliesTopAnomalyScoreDescription": "最大异常分数", - "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最大异常分数:", "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大异常分数:{maxAnomalyScore}", "xpack.infra.logs.analysis.recreateJobButtonLabel": "重新创建 ML 作业", "xpack.infra.logs.analysis.setupStatusTryAgainButton": "重试", @@ -7556,10 +7548,6 @@ "xpack.infra.logs.logEntryCategories.countColumnTitle": "消息计数", "xpack.infra.logs.logEntryCategories.datasetColumnTitle": "数据集", "xpack.infra.logs.logEntryCategories.datasetFilterPlaceholder": "按数据集筛选", - "xpack.infra.logs.logEntryCategories.exampleEmptyDescription": "选定时间范围内未找到任何示例。增大日志条目保留期限以改善消息样例可用性。", - "xpack.infra.logs.logEntryCategories.exampleEmptyReloadButtonLabel": "重新加载", - "xpack.infra.logs.logEntryCategories.exampleLoadingFailureDescription": "无法加载类别示例。", - "xpack.infra.logs.logEntryCategories.exampleLoadingFailureRetryButtonLabel": "重试", "xpack.infra.logs.logEntryCategories.jobStatusLoadingMessage": "正在检查归类作业的状态......", "xpack.infra.logs.logEntryCategories.loadDataErrorTitle": "无法加载类别数据", "xpack.infra.logs.logEntryCategories.manyCategoriesWarningReasonDescription": "每个分析文档的类别比率非常高,达到 {categoriesDocumentRatio, number }。", @@ -8473,13 +8461,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": "正在保存......", @@ -9794,8 +9776,6 @@ "xpack.ml.explorer.jobIdLabel": "作业 ID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)", "xpack.ml.explorer.kueryBar.filterPlaceholder": "按影响因素字段筛选……({queryExample})", - "xpack.ml.explorer.limitLabel": "限制", - "xpack.ml.explorer.loadingLabel": "正在加载", "xpack.ml.explorer.noConfiguredInfluencersTooltip": "“顶级影响因素”列表被隐藏,因为没有为所选作业配置影响因素。", "xpack.ml.explorer.noInfluencersFoundTitle": "未找到任何 {viewBySwimlaneFieldName} 影响因素", "xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "对于指定筛选找不到任何 {viewBySwimlaneFieldName} 影响因素", 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 957c79a5c512..655f64995d14 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 053541d84c43..08616b2895a8 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 58a9685fc73d..39c59a10fbc8 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 1f8bfde2cd22..4a6c13bf7f1a 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 0b095cdc2698..fd6a3d64bd4b 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 94bea3c51760..1b26b1157add 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 f0b131deb149..c8ad5f5b7080 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 cb905023cae4..1849a7ec9817 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 c19aec2a993d..b79fa0ea9405 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 6efa8d64dafb..8777c8f48e0f 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 5789d37a6bcf..80a2f9d7709c 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 665114bd86e9..9e57d7ae608c 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 9e802b96e16b..1dfd9e3edc2c 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/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index 244d431930f2..a282fa08e8f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -160,7 +160,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ setLoadingState(LoadingStateType.Idle); } })(); - /* eslint-disable react-hooks/exhaustive-deps */ + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ index, timeField, @@ -175,12 +175,12 @@ export const ThresholdVisualization: React.FunctionComponent = ({ threshold, startVisualizationAt, ]); - /* eslint-enable react-hooks/exhaustive-deps */ if (!charts || !uiSettings || !dataFieldsFormats) { return null; } const chartsTheme = charts.theme.useChartsTheme(); + const chartsBaseTheme = charts.theme.useChartsBaseTheme(); const domain = getDomain(alertInterval, startVisualizationAt); const visualizeOptions = { @@ -261,6 +261,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ 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 000000000000..0b8a9349ad5f --- /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 000000000000..e280fd3f34e9 --- /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 463d170df229..838b684cc10e 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 3a25417f7db4..66a7ac25d4a7 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 9deeeb96124c..799886d26454 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 0bec188f6364..da332aa326cc 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 6000af5498cd..841415620d69 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 aeb5801c9f6b..b7eafb7bf5c8 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/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap index 71690432fd01..d8235765bda2 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap @@ -123,7 +123,6 @@ exports[`ChartWrapper component renders the component with loading false 1`] = ` down={8} height={144} up={4} - width={144} />
@@ -252,7 +251,6 @@ exports[`ChartWrapper component renders the component with loading true 1`] = ` down={8} height={144} up={4} - width={144} />
- + baseTheme={ + Object { + "arcSeriesStyle": Object { + "arc": Object { + "opacity": 1, + "stroke": "black", + "strokeWidth": 1, + "visible": true, + }, + }, + "areaSeriesStyle": Object { + "area": Object { + "opacity": 0.3, + "visible": true, + }, + "line": Object { + "opacity": 1, + "strokeWidth": 1, + "visible": true, + }, + "point": Object { + "fill": "white", + "opacity": 1, + "radius": 2, + "strokeWidth": 1, + "visible": false, + }, + }, + "axes": Object { + "axisLineStyle": Object { + "stroke": "#eaeaea", + "strokeWidth": 1, + }, + "axisTitleStyle": Object { + "fill": "#333", + "fontFamily": "sans-serif", + "fontSize": 12, + "fontStyle": "bold", + "padding": 8, + }, + "gridLineStyle": Object { + "horizontal": Object { + "dash": Array [ + 0, + 0, + ], + "opacity": 1, + "stroke": "#D3DAE6", + "strokeWidth": 1, + "visible": true, + }, + "vertical": Object { + "dash": Array [ + 0, + 0, + ], + "opacity": 1, + "stroke": "#D3DAE6", + "strokeWidth": 1, + "visible": true, + }, + }, + "tickLabelStyle": Object { + "fill": "#777", + "fontFamily": "sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "padding": 4, + }, + "tickLineStyle": Object { + "stroke": "#eaeaea", + "strokeWidth": 1, + "visible": true, + }, + }, + "background": Object { + "color": "transparent", + }, + "barSeriesStyle": Object { + "displayValue": Object { + "fill": "#777", + "fontFamily": "sans-serif", + "fontSize": 8, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": Object { + "opacity": 1, + }, + "rectBorder": Object { + "strokeWidth": 0, + "visible": false, + }, + }, + "bubbleSeriesStyle": Object { + "point": Object { + "fill": "white", + "opacity": 1, + "radius": 2, + "strokeWidth": 1, + "visible": true, + }, + }, + "chartMargins": Object { + "bottom": 10, + "left": 10, + "right": 10, + "top": 10, + }, + "chartPaddings": Object { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, + }, + "colors": Object { + "defaultVizColor": "red", + "vizColors": Array [ + "#1EA593", + "#2B70F7", + "#CE0060", + "#38007E", + "#FCA5D3", + "#F37020", + "#E49E29", + "#B0916F", + "#7B000B", + "#34130C", + ], + }, + "crosshair": Object { + "band": Object { + "fill": "#F5F5F5", + "visible": true, + }, + "line": Object { + "dash": Array [ + 5, + 5, + ], + "stroke": "#777", + "strokeWidth": 1, + "visible": true, + }, + }, + "legend": Object { + "horizontalHeight": 64, + "spacingBuffer": 10, + "verticalWidth": 200, + }, + "lineSeriesStyle": Object { + "line": Object { + "opacity": 1, + "strokeWidth": 1, + "visible": true, + }, + "point": Object { + "fill": "white", + "opacity": 1, + "radius": 2, + "strokeWidth": 1, + "visible": true, + }, + }, + "scales": Object { + "barsPadding": 0.25, + "histogramPadding": 0.05, + }, + "sharedStyle": Object { + "default": Object { + "opacity": 1, + }, + "highlighted": Object { + "opacity": 1, + }, + "unhighlighted": Object { + "opacity": 0.25, + }, + }, + } + } + renderer="canvas" + size={125} + theme={ + Object { + "areaSeriesStyle": Object { + "area": Object { + "opacity": 0.3, + }, + "line": Object { + "strokeWidth": 2, + }, + "point": Object { + "fill": "rgba(255, 255, 255, 1)", + "radius": 3, + "strokeWidth": 2, + "visible": false, + }, + }, + "axes": Object { + "axisLineStyle": Object { + "stroke": "rgba(238, 240, 243, 1)", + }, + "axisTitleStyle": Object { + "fill": "rgba(52, 55, 65, 1)", + "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + "fontSize": 12, + "padding": 10, + }, + "gridLineStyle": Object { + "horizontal": Object { + "dash": Array [ + 0, + 0, + ], + "opacity": 1, + "stroke": "rgba(238, 240, 243, 1)", + "strokeWidth": 1, + "visible": true, + }, + "vertical": Object { + "dash": Array [ + 4, + 4, + ], + "opacity": 1, + "stroke": "rgba(238, 240, 243, 1)", + "strokeWidth": 1, + "visible": true, + }, + }, + "tickLabelStyle": Object { + "fill": "rgba(105, 112, 125, 1)", + "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + "fontSize": 10, + "padding": 8, + }, + "tickLineStyle": Object { + "stroke": "rgba(238, 240, 243, 1)", + "strokeWidth": 1, + "visible": false, + }, + }, + "barSeriesStyle": Object { + "displayValue": Object { + "fill": "rgba(105, 112, 125, 1)", + "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + "fontSize": 8, + }, + }, + "chartMargins": Object { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, + }, + "colors": Object { + "defaultVizColor": "#6092C0", + "vizColors": Array [ + "#54B399", + "#6092C0", + "#9170B8", + "#CA8EAE", + "#D36086", + "#E7664C", + "#AA6556", + "#DA8B45", + "#B9A888", + "#D6BF57", + ], + }, + "crosshair": Object { + "band": Object { + "fill": "rgba(245, 247, 250, 1)", + }, + "line": Object { + "dash": Array [ + 4, + 4, + ], + "stroke": "rgba(105, 112, 125, 1)", + "strokeWidth": 1, + }, + }, + "lineSeriesStyle": Object { + "line": Object { + "strokeWidth": 2, + }, + "point": Object { + "fill": "rgba(255, 255, 255, 1)", + "radius": 3, + "strokeWidth": 2, + }, + }, + "scales": Object { + "barsPadding": 0.25, + "histogramPadding": 0.05, + }, + } + } + > + + +
- +
+
+
+
+
+
- +
+
+
+
+
+
{ it('renders the component with loading false', () => { @@ -20,7 +19,7 @@ describe('ChartWrapper component', () => { - + ); expect(component).toMatchSnapshot(); @@ -31,7 +30,7 @@ describe('ChartWrapper component', () => { - + ); expect(component).toMatchSnapshot(); @@ -42,7 +41,7 @@ describe('ChartWrapper component', () => { - + ); @@ -64,7 +63,7 @@ describe('ChartWrapper component', () => { - + ); diff --git a/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx index b4d6864423df..19716f0d3b1c 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx @@ -5,10 +5,10 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import React, { useContext, useEffect, useRef } from 'react'; -import * as d3 from 'd3'; +import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { Chart, Datum, Partition, Settings, PartitionLayout } from '@elastic/charts'; import { DonutChartLegend } from './donut_chart_legend'; import { UptimeThemeContext } from '../../../contexts'; @@ -16,7 +16,6 @@ interface DonutChartProps { down: number; height: number; up: number; - width: number; } export const GreenCheckIcon = styled(EuiIcon)` @@ -28,72 +27,56 @@ export const GreenCheckIcon = styled(EuiIcon)` position: absolute; `; -export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { - const chartElement = useRef(null); - +export const DonutChart = ({ height, down, up }: DonutChartProps) => { const { colors: { danger, gray }, + chartTheme, } = useContext(UptimeThemeContext); - let upCount = up; - if (up === 0 && down === 0) { - upCount = 1; - } - useEffect(() => { - if (chartElement.current !== null) { - // we must remove any existing paths before painting - d3.select(chartElement.current).selectAll('g').remove(); - - const svgElement = d3 - .select(chartElement.current) - .append('g') - .attr('transform', `translate(${width / 2}, ${height / 2})`); - - const color = d3.scale.ordinal().domain(['up', 'down']).range([gray, danger]); - - const pieGenerator = d3.layout - .pie() - .value(({ value }: any) => value) - // these start/end angles will reverse the direction of the pie, - // which matches our design - .startAngle(2 * Math.PI) - .endAngle(0); - - svgElement - .selectAll('g') - .data( - // @ts-ignore pie generator expects param of type number[], but only works with - // output of d3.entries, which is like Array<{ key: string, value: number }> - pieGenerator(d3.entries({ up: upCount, down })) - ) - .enter() - .append('path') - .attr( - 'd', - // @ts-ignore attr does not expect a param of type Arc but it behaves as desired - d3.svg - .arc() - .innerRadius(width * 0.28) - .outerRadius(Math.min(width, height) / 2 - 10) - ) - .attr('fill', (d: any) => color(d.data.key) as any); - } - }, [danger, down, gray, height, upCount, width]); - return ( - - {/* When all monitors are up we show green check icon in middle of donut to indicate, all is well */} + {...chartTheme} + > + + d.value as number} + layers={[ + { + groupByRollup: (d: Datum) => d.label, + nodeLabel: (d: Datum) => d, + shape: { + fillColor: (d: Datum) => { + return d.dataName === 'Down' ? danger : gray; + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maximumSection: Infinity, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + idealFontSizeJump: 1.1, + outerSizeRatio: 0.9, + emptySizeRatio: 0.4, + circlePadding: 4, + }} + /> + {down === 0 && } diff --git a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx index a514013eeed9..cbbffdff745f 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx @@ -12,7 +12,7 @@ import { DonutChartLegendRow } from './donut_chart_legend_row'; import { UptimeThemeContext } from '../../../contexts'; const LegendContainer = styled.div` - max-width: 260px; + max-width: 150px; min-width: 100px; @media (max-width: 767px) { min-width: 0px; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap index 9496274a6917..823346db3518 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap @@ -111,13 +111,13 @@ Array [ -

au-heartbeat

-
+
@@ -188,13 +188,13 @@ Array [ -

nyc-heartbeat

-
+
@@ -265,13 +265,13 @@ Array [ -

spa-heartbeat

-
+
@@ -356,18 +356,21 @@ exports[`AvailabilityReporting component shallow renders correctly against snaps "availability": 100, "color": "#d3dae6", "label": "au-heartbeat", + "status": "up", "timestamp": "36m ago", }, Object { "availability": 100, "color": "#d3dae6", "label": "nyc-heartbeat", + "status": "down", "timestamp": "36m ago", }, Object { "availability": 100, "color": "#d3dae6", "label": "spa-heartbeat", + "status": "down", "timestamp": "36m ago", }, ] diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap index 05e0b50a86f3..4d3e85ba18eb 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap @@ -10,18 +10,21 @@ exports[`LocationStatusTags component renders properly against props 1`] = ` "availability": 100, "color": "#d3dae6", "label": "Berlin", + "status": "up", "timestamp": "1 Mon ago", }, Object { "availability": 100, "color": "#bd271e", "label": "Berlin", + "status": "down", "timestamp": "1 Mon ago", }, Object { "availability": 100, "color": "#d3dae6", "label": "Islamabad", + "status": "up", "timestamp": "1 Mon ago", }, ] @@ -145,13 +148,13 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = -

Berlin

-
+
@@ -222,13 +225,13 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = -

Islamabad

-
+
@@ -393,13 +396,13 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` -

Berlin

-
+
@@ -470,13 +473,13 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` -

Islamabad

-
+
@@ -641,13 +644,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Berlin

-
+
@@ -718,13 +721,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Islamabad

-
+
@@ -795,13 +798,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

New York

-
+
@@ -872,13 +875,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Paris

-
+
@@ -949,13 +952,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Sydney

-
+
diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap index 3381efa62286..28f1f433648c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap @@ -26,13 +26,13 @@ exports[`TagLabel component renders correctly against snapshot 1`] = ` -

US-East

-
+
@@ -44,13 +44,13 @@ exports[`TagLabel component shallow render correctly against snapshot 1`] = ` -

US-East

-
+
`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx index de9f6b0d3b30..b5fe5d17312c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx @@ -19,14 +19,22 @@ describe('AvailabilityReporting component', () => { timestamp: '36m ago', color: '#d3dae6', availability: 100, + status: 'up', }, { label: 'nyc-heartbeat', timestamp: '36m ago', color: '#d3dae6', availability: 100, + status: 'down', + }, + { + label: 'spa-heartbeat', + timestamp: '36m ago', + color: '#d3dae6', + availability: 100, + status: 'down', }, - { label: 'spa-heartbeat', timestamp: '36m ago', color: '#d3dae6', availability: 100 }, ]; }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx index 356078412229..8e46196ec3ab 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx @@ -10,12 +10,12 @@ import { TagLabel } from '../tag_label'; describe('TagLabel component', () => { it('shallow render correctly against snapshot', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); it('renders correctly against snapshot', () => { - const component = renderWithIntl(); + const component = renderWithIntl(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx index 8fed5db5e027..ccf7d41642bf 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx @@ -30,7 +30,7 @@ export const AvailabilityReporting: React.FC = ({ allLocations }) => { name: LocationLabel, truncateText: true, render: (val: string, item: StatusTag) => { - return ; + return ; }, }, { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx index 6096499213a1..b48252d4208d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx @@ -24,9 +24,10 @@ interface Props { export interface StatusTag { label: string; - timestamp: string; + timestamp?: string; color: string; - availability: number; + availability?: number; + status: 'up' | 'down'; } export const LocationStatusTags = ({ locations }: Props) => { @@ -48,6 +49,7 @@ export const LocationStatusTags = ({ locations }: Props) => { timestamp: moment(new Date(item.timestamp).valueOf()).fromNow(), color: item.summary.down === 0 ? gray : danger, availability: (item.up_history / (item.up_history + item.down_history)) * 100, + status: item.summary.down === 0 ? 'up' : 'down', }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx index dbd73fc7d440..ec5718415595 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx @@ -6,7 +6,8 @@ import React from 'react'; import styled from 'styled-components'; -import { EuiBadge, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiTextColor } from '@elastic/eui'; +import { StatusTag } from './location_status_tags'; const BadgeItem = styled.div` white-space: nowrap; @@ -17,18 +18,13 @@ const BadgeItem = styled.div` } `; -interface Props { - color: string; - label: string; -} - -export const TagLabel: React.FC = ({ color, label }) => { +export const TagLabel: React.FC = ({ color, label, status }) => { return ( - +

{label}

-
+
); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx index bf403846dcec..8c66b57de58a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx @@ -64,9 +64,9 @@ export const MapToolTipComponent = ({ closeTooltip, features = [] }: MapToolTipP <> {layerId === 'up_points' ? ( - + ) : ( - + )} diff --git a/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap index db41dfb0b04c..2135fc32c2b5 100644 --- a/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap @@ -14,7 +14,6 @@ exports[`Snapshot component renders without errors 1`] = ` down={2} height={144} up={8} - width={144} /> `; 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 25cf400bcd0f..1c587568fe61 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 bbcba7238748..55a99ab8541f 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 12fff376a1a3..26d009344646 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 a9d04e68b892..f80c73dcf5bb 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/components/overview/snapshot/snapshot.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx index 8d6933ad18ce..bc54f14e8782 100644 --- a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx +++ b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx @@ -11,7 +11,6 @@ import { ChartWrapper } from '../../common/charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; -const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; interface SnapshotComponentProps { @@ -29,11 +28,6 @@ export const SnapshotComponent: React.FC = ({ count, hei - + ); 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 3a940b4655b1..d6185f2c2589 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 414b37939b38..397d23a18332 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 793a671f5fed..a4b85312849a 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 000000000000..dd7996b68c41 --- /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 6e52b3a11f25..f5c4c55a4e30 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 b82f631a30fe..01702a033d58 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 9dc162e06fba..189da025e714 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 2bdd03e23c6d..90cb65a77e9a 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 71057b81d1b0..e14a85d6e30c 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'); }); @@ -90,7 +90,7 @@ export default function (providerContext: FtrProviderContext) { events: [ { type: 'ACTION_RESULT', - subtype: 'CONFIG', + subtype: 'ACKNOWLEDGED', timestamp: '2019-01-04T14:32:03.36764-05:00', action_id: configChangeAction.id, agent_id: enrollmentResponse.item.id, @@ -132,7 +132,43 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(unenrollResponse.success).to.eql(true); - // Checkin after unenrollment + // Checkin after unenrollment + const { body: checkinAfterUnenrollResponse } = await supertestWithoutAuth + .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/checkin`) + .set('kbn-xsrf', 'xx') + .set('Authorization', `ApiKey ${agentAccessAPIKey}`) + .send({ + events: [], + }) + .expect(200); + + expect(checkinAfterUnenrollResponse.success).to.eql(true); + expect(checkinAfterUnenrollResponse.actions).length(1); + expect(checkinAfterUnenrollResponse.actions[0].type).be('UNENROLL'); + const unenrollAction = checkinAfterUnenrollResponse.actions[0]; + + // ack unenroll actions + const { body: ackUnenrollApiResponse } = await supertestWithoutAuth + .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/acks`) + .set('Authorization', `ApiKey ${agentAccessAPIKey}`) + .set('kbn-xsrf', 'xx') + .send({ + events: [ + { + type: 'ACTION_RESULT', + subtype: 'ACKNOWLEDGED', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: unenrollAction.id, + agent_id: enrollmentResponse.item.id, + message: 'hello', + payload: 'payload', + }, + ], + }) + .expect(200); + expect(ackUnenrollApiResponse.success).to.eql(true); + + // Checkin after unenrollment acknowledged await supertestWithoutAuth .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/checkin`) .set('kbn-xsrf', 'xx') diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index ecc39ea64558..bc6c44e590cc 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -67,7 +67,7 @@ export default function (providerContext: FtrProviderContext) { .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ - ids: ['agent1'], + force: true, }) .expect(200); @@ -80,7 +80,7 @@ export default function (providerContext: FtrProviderContext) { .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ - ids: ['agent1'], + force: true, }) .expect(200); 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 292aabad8505..a563b956df34 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 003fb21b09cc..3a3d73ab6841 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 }); - // blocking es snapshot promotion: https://github.com/elastic/kibana/issues/70532 - describe.skip('index templates', () => { - after(() => Promise.all([cleanUpEsResources()])); + describe('index templates', () => { + 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 templates - expect(allTemplates.templates).to.eql([]); - - // 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/log_sources.ts b/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts index d4cdd7316b3f..00af3f8c2510 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts @@ -35,7 +35,7 @@ export default function ({ getService }: FtrProviderContext) { expect(origin).to.be('fallback'); expect(configuration.name).to.be('Default'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.timestamp).to.be('@timestamp'); expect(configuration.fields.tiebreaker).to.be('_doc'); expect(configuration.logColumns[0]).to.have.key('timestampColumn'); @@ -97,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration.name).to.be('Default'); expect(origin).to.be('stored'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.timestamp).to.be('@timestamp'); expect(configuration.fields.tiebreaker).to.be('_doc'); expect(configuration.logColumns).to.have.length(3); @@ -166,7 +166,7 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration.name).to.be('NAME'); expect(origin).to.be('stored'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.timestamp).to.be('@timestamp'); expect(configuration.fields.tiebreaker).to.be('_doc'); expect(configuration.logColumns).to.have.length(3); 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 673ee3c3f847..23b0a96ecd40 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 554b17d505c5..d372496d2d1d 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 1f4da602b108..bb0934b73a4c 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/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts index 5ed038776625..5908523af249 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts @@ -40,8 +40,8 @@ export default function ({ getService }: FtrProviderContext) { // shipped default values expect(sourceConfiguration.name).to.be('Default'); - expect(sourceConfiguration.metricAlias).to.be('metricbeat-*'); - expect(sourceConfiguration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(sourceConfiguration.metricAlias).to.be('metrics-*,metricbeat-*'); + expect(sourceConfiguration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(sourceConfiguration.fields.container).to.be('container.id'); expect(sourceConfiguration.fields.host).to.be('host.name'); expect(sourceConfiguration.fields.pod).to.be('kubernetes.pod.uid'); @@ -125,8 +125,8 @@ export default function ({ getService }: FtrProviderContext) { expect(updatedAt).to.be.greaterThan(0); expect(configuration.name).to.be('NAME'); expect(configuration.description).to.be(''); - expect(configuration.metricAlias).to.be('metricbeat-*'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.metricAlias).to.be('metrics-*,metricbeat-*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.container).to.be('container.id'); expect(configuration.fields.host).to.be('host.name'); expect(configuration.fields.pod).to.be('kubernetes.pod.uid'); @@ -283,7 +283,7 @@ export default function ({ getService }: FtrProviderContext) { expect(version).to.not.be(initialVersion); expect(updatedAt).to.be.greaterThan(createdAt); expect(configuration.metricAlias).to.be('metricbeat-**'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(status.logIndicesExist).to.be(true); expect(status.metricIndicesExist).to.be(true); }); diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 1c98cd3a4e37..10c0f00234ab 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -218,7 +218,7 @@ export default ({ getService }: FtrProviderContext) => { responseCode: 200, jobs: [ { - jobId: 'pf5_high_mean_response_time', + jobId: 'pf5_high_mean_transaction_duration', jobState: JOB_STATE.CLOSED, datafeedState: DATAFEED_STATE.STOPPED, modelMemoryLimit: '11mb', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index de0abe2350eb..78915f658029 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 00bfcdc119e4..d2bfdbe4dc96 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 cd78f0ff7b88..662879c49523 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/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index b5b0197aad4b..eab90e1fc19c 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -13,8 +13,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); const browser = getService('browser'); - // blocking es snapshot promotion: https://github.com/elastic/kibana/issues/70532 - describe.skip('Home page', function () { + describe('Home page', function () { before(async () => { await pageObjects.common.navigateToApp('indexManagement'); }); @@ -82,9 +81,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const url = await browser.getCurrentUrl(); expect(url).to.contain(`/component_templates`); - // There should be no component templates by default, so we verify the empty prompt displays - const componentTemplateEmptyPrompt = await testSubjects.exists('emptyList'); - expect(componentTemplateEmptyPrompt).to.be(true); + // Verify content. Component templates may have been created by other apps, e.g. Ingest Manager, + // so we don't make any assertion about the presence or absence of component templates. + const componentTemplateList = await testSubjects.exists('componentTemplateList'); + expect(componentTemplateList).to.be(true); }); }); }); diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index b980116c581d..9146ec733462 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 b399c9e915e2..97cdd081705a 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 c0a1ff34db96..d8a3e40ccc01 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'); @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }) { const screenshot = getService('screenshots'); const PageObjects = getPageObjects(['security', 'common', 'header', 'discover', 'settings']); - describe('dls', function () { + // Skipped as failing on ES Promotion: https://github.com/elastic/kibana/issues/70818 + describe.skip('dls', function () { before('initialize tests', async () => { await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('security/dlstest'); @@ -43,7 +44,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 +62,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 ec6b91219b7c..7b22d72885c9 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 e9c09d1af6ea..b138859d0136 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 94b430681905..2054a7b0b003 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 b6e3a84d6f7c..a2a2b705172d 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 4c8c04c0356b..4fd4384a93c5 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/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 04a751279bf3..6dd22a1f4a20 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -5,9 +5,10 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, loadTestFile }: FtrProviderContext) { +export default function ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); + const PageObjects = getPageObjects(['security']); describe('transform', function () { this.tags(['ciGroup9', 'transform']); @@ -30,6 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('ml/ecommerce'); await transform.testResources.resetKibanaTimeZone(); + await PageObjects.security.logout(); }); loadTestFile(require.resolve('./creation_index_pattern')); diff --git a/x-pack/test/functional/apps/watcher/watcher_test.js b/x-pack/test/functional/apps/watcher/watcher_test.js index 7a2eebc118ab..1dd3fb6bbcc3 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/endpoint/artifacts/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json index a886b60e7e0d..b156f2f6cc7b 100644 --- a/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json +++ b/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json @@ -1,20 +1,23 @@ { "type": "doc", "value": { - "id": "endpoint:exceptions-artifact:endpoint-exceptionlist-linux-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", + "id": "endpoint:user-artifact:endpoint-exceptionlist-linux-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", "index": ".kibana", "source": { "references": [ ], - "endpoint:exceptions-artifact": { + "endpoint:user-artifact": { "body": "eyJleGNlcHRpb25zX2xpc3QiOltdfQ==", "created": 1593016187465, - "encoding": "application/json", + "compressionAlgorithm": "none", + "encryptionAlgorithm": "none", "identifier": "endpoint-exceptionlist-linux-1.0.0", - "sha256": "a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", - "size": 22 + "compressedSha256": "a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", + "compressedSize": 22, + "decompressedSha256": "a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", + "decompressedSize": 22 }, - "type": "endpoint:exceptions-artifact", + "type": "endpoint:user-artifact", "updated_at": "2020-06-24T16:29:47.584Z" } } @@ -23,12 +26,12 @@ { "type": "doc", "value": { - "id": "endpoint:exceptions-manifest:endpoint-manifest-1.0.0", + "id": "endpoint:user-artifact-manifest:endpoint-manifest-1.0.0", "index": ".kibana", "source": { "references": [ ], - "endpoint:exceptions-manifest": { + "endpoint:user-artifact-manifest": { "created": 1593183699663, "ids": [ "endpoint-exceptionlist-linux-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", @@ -36,7 +39,7 @@ "endpoint-exceptionlist-windows-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d" ] }, - "type": "endpoint:exceptions-manifest", + "type": "endpoint:user-artifact-manifest", "updated_at": "2020-06-26T15:01:39.704Z" } } 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 c317aad8ba05..b3d49199b0d9 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/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts index 7c479a423467..80df235bf6ff 100644 --- a/x-pack/test/functional/services/ml/anomaly_explorer.ts +++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts @@ -76,7 +76,7 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid async addAndEditSwimlaneInDashboard(dashboardTitle: string) { await this.filterWithSearchString(dashboardTitle); await testSubjects.isDisplayed('mlDashboardSelectionTable > checkboxSelectAll'); - await testSubjects.click('mlDashboardSelectionTable > checkboxSelectAll'); + await testSubjects.clickWhenNotDisabled('mlDashboardSelectionTable > checkboxSelectAll'); expect(await testSubjects.isChecked('mlDashboardSelectionTable > checkboxSelectAll')).to.be( true ); 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 13bf47676cc0..2225316bba80 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 e22b098e6ee0..d86d272c1da8 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 35a420561731..2b174a8cae07 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 9fbe9f26944c..ba6d7ced3c59 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/plugin_functional/es_archives/global_search/basic/data.json b/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json new file mode 100644 index 000000000000..f121f6859885 --- /dev/null +++ b/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json @@ -0,0 +1,193 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:i-exist", + "source": { + "dashboard": { + "title": "Amazing Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "map:0b849ed0-70f5-11e9-8625-9580c4904684", + "source": { + "map": { + "title" : "just a map", + "description" : "", + "mapStateJSON" : "{\"zoom\":0.8,\"center\":{\"lon\":0,\"lat\":0},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"}}", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"c7bdee60-5267-459e-83d6-b53acf1b9e67\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"applyGlobalQuery\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"}]", + "uiStateJSON" : "{\"isLayerTOCOpen\":true}", + "bounds" : { + "type" : "polygon", + "coordinates" : [ + [ + [ + -180, + 85.05113 + ], + [ + -180, + -85.05113 + ], + [ + 180, + -85.05113 + ], + [ + 180, + 85.05113 + ], + [ + -180, + 85.05113 + ] + ] + ] + } + }, + "type": "map", + "references" : [ ], + "migrationVersion" : { + "map" : "7.1.0" + }, + "updated_at" : "2019-05-07T18:22:17.405Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:1c1a87f0-70f5-11e9-8625-9580c4904684", + "source": { + "dashboard": { + "title" : "dashboard with map", + "hits" : 0, + "description" : "", + "panelsJSON" : "[{\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"version\":\"8.0.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":0,\"lon\":0,\"zoom\":0.8},\"isLayerTOCOpen\":true},\"panelRefName\":\"panel_0\"}]", + "optionsJSON" : "{\"useMargins\":true,\"hidePanelTitles\":false}", + "version" : 1, + "timeRestore" : false, + "kibanaSavedObjectMeta" : { + "searchSourceJSON" : "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "references" : [ + { + "name" : "panel_0", + "type" : "map", + "id" : "0b849ed0-70f5-11e9-8625-9580c4904684" + } + ], + "migrationVersion" : { + "dashboard" : "7.0.0" + }, + "updated_at" : "2019-05-07T18:22:45.231Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "query:okjpgs", + "source": { + "query": { + "title": "OKJpgs", + "description": "Ok responses for jpg files", + "query": { + "query": "response:200", + "language": "kuery" + }, + "filters": [{"meta":{"index":"b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b","alias":null,"negate":false,"type":"phrase","key":"extension.raw","value":"jpg","params":{"query":"jpg"},"disabled":false},"query":{"match":{"extension.raw":{"query":"jpg","type":"phrase"}}},"$state":{"store":"appState"}}] + }, + "type": "query", + "updated_at": "2019-07-17T17:54:26.378Z" + } + } +} diff --git a/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json b/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json new file mode 100644 index 000000000000..ebb5b19387fa --- /dev/null +++ b/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json @@ -0,0 +1,516 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "references": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "query": { + "properties": { + "title": { + "type": "text" + }, + "description": { + "type": "text" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "type": "keyword", + "index": false + } + } + }, + "filters": { + "type": "object", + "enabled": false + }, + "timefilter": { + "type": "object", + "enabled": false + } + } + } + } + } + } +} diff --git a/x-pack/test/plugin_functional/plugins/global_search_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/global_search_test/public/plugin.ts index 27434202d77f..aba3512788f9 100644 --- a/x-pack/test/plugin_functional/plugins/global_search_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/global_search_test/public/plugin.ts @@ -17,7 +17,8 @@ import { createResult } from '../common/utils'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface GlobalSearchTestPluginSetup {} export interface GlobalSearchTestPluginStart { - findAll: (term: string) => Promise; + findTest: (term: string) => Promise; + findReal: (term: string) => Promise; } export interface GlobalSearchTestPluginSetupDeps { @@ -74,7 +75,7 @@ export class GlobalSearchTestPlugin { globalSearch }: GlobalSearchTestPluginStartDeps ): GlobalSearchTestPluginStart { return { - findAll: (term) => + findTest: (term) => globalSearch .find(term, {}) .pipe( @@ -84,6 +85,16 @@ export class GlobalSearchTestPlugin reduce((memo, results) => [...memo, ...results]) ) .toPromise(), + findReal: (term) => + globalSearch + .find(term, {}) + .pipe( + map((batch) => batch.results), + // remove test types + map((results) => results.filter((r) => !r.type.startsWith('test_'))), + reduce((memo, results) => [...memo, ...results]) + ) + .toPromise(), }; } } diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_api.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_api.ts index ee1745436b73..841c4d2967e2 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_api.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_api.ts @@ -17,7 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { return browser.executeAsync(async (term, cb) => { const { start } = window.__coreProvider; const globalSearchTestApi: GlobalSearchTestApi = start.plugins.globalSearchTest; - globalSearchTestApi.findAll(term).then(cb); + globalSearchTestApi.findTest(term).then(cb); }, t); }; diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts new file mode 100644 index 000000000000..4e4f42578d11 --- /dev/null +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { GlobalSearchResult } from '../../../../plugins/global_search/common/types'; +import { GlobalSearchTestApi } from '../../plugins/global_search_test/public/types'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + const findResultsWithAPI = async (t: string): Promise => { + return browser.executeAsync(async (term, cb) => { + const { start } = window.__coreProvider; + const globalSearchTestApi: GlobalSearchTestApi = start.plugins.globalSearchTest; + globalSearchTestApi.findReal(term).then(cb); + }, t); + }; + + describe('GlobalSearch - SavedObject provider', function () { + before(async () => { + await esArchiver.load('global_search/basic'); + }); + + after(async () => { + await esArchiver.unload('global_search/basic'); + }); + + beforeEach(async () => { + await pageObjects.common.navigateToApp('globalSearchTestApp'); + }); + + it('can search for index patterns', async () => { + const results = await findResultsWithAPI('logstash'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('index-pattern'); + expect(results[0].title).to.be('logstash-*'); + expect(results[0].score).to.be.greaterThan(1); + }); + + it('can search for visualizations', async () => { + const results = await findResultsWithAPI('pie'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('visualization'); + expect(results[0].title).to.be('A Pie'); + }); + + it('can search for maps', async () => { + const results = await findResultsWithAPI('just'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('map'); + expect(results[0].title).to.be('just a map'); + }); + + it('can search for dashboards', async () => { + const results = await findResultsWithAPI('Amazing'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('dashboard'); + expect(results[0].title).to.be('Amazing Dashboard'); + }); + + it('returns all objects matching the search', async () => { + const results = await findResultsWithAPI('dashboard'); + expect(results.length).to.be.greaterThan(2); + expect(results.map((r) => r.title)).to.contain('dashboard with map'); + expect(results.map((r) => r.title)).to.contain('Amazing Dashboard'); + }); + }); +} diff --git a/x-pack/test/plugin_functional/test_suites/global_search/index.ts b/x-pack/test/plugin_functional/test_suites/global_search/index.ts index 1e5a765612f4..d765e87add10 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/index.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/index.ts @@ -10,5 +10,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('GlobalSearch API', function () { this.tags('ciGroup7'); loadTestFile(require.resolve('./global_search_api')); + loadTestFile(require.resolve('./global_search_providers')); }); } 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 45ea82c59bf9..bacba619e564 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 76c1e2259545..f2099e1eb7c9 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 4975e073eea1..e2b6693ce77a 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 1bf1370ad467..73efee0dab2e 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 ee61303e85f4..7e4478038953 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2144,10 +2144,10 @@ dependencies: "@elastic/apm-rum-core" "^5.3.0" -"@elastic/charts@19.5.2": - version "19.5.2" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.5.2.tgz#6117f7b3acce6deef0a4f272ae491d99d2e3b1e0" - integrity sha512-6GdqwVrDwQu+h5GUXpwy9a8dQ66oTNl3SO+ih1sljWvni+f/wcsrRcCTJUP99vtUcPQ8BT9Pn79QknBk1ZOH5Q== +"@elastic/charts@19.7.0": + version "19.7.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.7.0.tgz#86cdee072d70e641135de99646c90359992bfdf0" + integrity sha512-oNAPOpI9OkuX/pWL+SGShcmdAUB1mwbOyJnp9/PHFqXtARg3aaiTDD0olZUuynGKd6DWnN8mEAiwoe7nsWGP9g== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -2168,6 +2168,9 @@ ts-debounce "^1.0.0" utility-types "^3.10.0" uuid "^3.3.2" + optionalDependencies: + redux-immutable-state-invariant "^2.1.0" + redux-logger "^3.0.6" "@elastic/elasticsearch@^7.4.0": version "7.4.0" @@ -5322,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" @@ -12005,6 +11994,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ= + deep-eql@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" @@ -20664,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" @@ -20784,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" @@ -20864,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" @@ -20879,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" @@ -20904,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" @@ -20924,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" @@ -26502,6 +26456,21 @@ redux-devtools-extension@^2.13.8: resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg== +redux-immutable-state-invariant@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1" + integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg== + dependencies: + invariant "^2.1.0" + json-stringify-safe "^5.0.1" + +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8= + dependencies: + deep-diff "^0.3.5" + redux-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7"