diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index bd55bd73966fff..3986367d660a18 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/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index a3470cd750738d..aafdf06433c6d7 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 00000000000000..17d86d1f855218 --- /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/CONTRIBUTING.md b/CONTRIBUTING.md index a7345f4b2897b2..a0aeed7a34949d 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/api/using-api.asciidoc b/docs/api/using-api.asciidoc index aba65f2e921c28..e58d9c39ee8c4a 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -10,7 +10,7 @@ NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to inte [float] [[api-authentication]] === Authentication -{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. +{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, which is where the username and password are stored in order to be passed as part of the call. [float] [[api-calls]] 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 00000000000000..3f2d81cc97c7c8 --- /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 a7d13b0015e3fd..cb81686b424ecc 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 00000000000000..2a79eafd85a6c2 --- /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 9a31d37b3ff30e..b9e726eac799d7 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/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md index 1e0e89767c4e6a..c839dd16d9a475 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md @@ -4,7 +4,9 @@ ## SavedObjectTypeRegistry.getAllTypes() method -Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered. +Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones. + +To only get the visible types (which is the most common use case), use `getVisibleTypes` instead. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md new file mode 100644 index 00000000000000..a773c6a0a674fb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [getVisibleTypes](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md) + +## SavedObjectTypeRegistry.getVisibleTypes() method + +Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md). + +A visible type is a type that doesn't explicitly define `hidden=true` during registration. + +Signature: + +```typescript +getVisibleTypes(): SavedObjectsType[]; +``` +Returns: + +`SavedObjectsType[]` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md index 69a94e4ad8c882..55ad7ca137de0a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md @@ -16,10 +16,11 @@ export declare class SavedObjectTypeRegistry | Method | Modifiers | Description | | --- | --- | --- | -| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered. | +| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones.To only get the visible types (which is the most common use case), use getVisibleTypes instead. | | [getImportableAndExportableTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered that are importable/exportable. | | [getIndex(type)](./kibana-plugin-core-server.savedobjecttyperegistry.getindex.md) | | Returns the indexPattern property for given type, or undefined if the type is not registered. | | [getType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition for given type name. | +| [getVisibleTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md) | | Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md).A visible type is a type that doesn't explicitly define hidden=true during registration. | | [isHidden(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md) | | Returns the hidden property for given type, or false if the type is not registered. | | [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the management.importableAndExportable property for given type, or false if the type is not registered or does not define a management section. | | [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable); resolves to false if the type is not registered | 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 5fa7d4841537b9..48ec9456c56dd8 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 cd07596ad37ef5..8fc2b7381de835 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 928878fdcdb03a..c83cd068eff5b3 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 b520be4df69696..3c15d9ee3c97b5 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.6.3", "@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 015dca128ce914..10b607dcd43125 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 ea72a4a48caeba..c6bb06e68b9c04 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 25b122f400711a..16572cf494cd3c 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 9ff0f563442749..5d5c5e3b6eb748 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 00000000000000..63beb9927b9f57 --- /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 00000000000000..4040cab1878fc6 --- /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 00000000000000..3918413c068639 --- /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 7d021a5ee78475..89cde2c1cd064f 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 00000000000000..019a9b7bdee3e6 --- /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 00000000000000..27b5e12b807a87 --- /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 a1ab51ee97c231..8726b3452ff1e1 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 b6b0973f0d5398..211cfac3806ad7 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -58,11 +58,15 @@ OptimizerConfig { ], "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "themeTags": Array [ + "v7dark", + "v7light", + ], "watch": false, } `; -exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { tap((state) => { if (state.event?.type === 'worker stdio') { // eslint-disable-next-line no-console - console.log('worker', state.event.stream, state.event.chunk.toString('utf8')); + console.log('worker', state.event.stream, state.event.line); } }), toArray() @@ -226,7 +226,7 @@ const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabe // Verify the brotli variant matches expect( - // @ts-ignore @types/node is missing the brotli functions + // @ts-expect-error @types/node is missing the brotli functions Zlib.brotliDecompressSync( Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.br`)) ).toString() diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts index cbec159bd27a02..23767be610da4f 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 9d7f1709506f9f..47d01347a8f7d1 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 00000000000000..9bf8f9db1fe450 --- /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 00000000000000..e8daecef8e0dd9 --- /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 fef3efc13a516a..31b34bd5c59385 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 d4152133f289de..5b46d67479fd58 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 c9e9b3ad01ccc6..7757004139d0d4 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 1572f459e6ee5f..09f8ca10c61818 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 de5e9372e9e7ac..ca7673748bde91 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 5d02462ef1bb87..f2f685bde65d96 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 11f5544cd9274d..aaea70d12c60d6 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 e61037e42d63f4..398b49fa1ecd5c 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 b9df67b32e5d30..5c1e98cd869de0 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 7fc29b1e6bd0a0..dc4d8a2fc10fbe 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 3e7ed49c613141..188db0a8321a26 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 96ce6fd1d919ab..cf4ecbb4ad42c9 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 534f503e2956a7..740ee3819c36f5 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 caeffaabea62be..b2df4f40d4fbe9 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 042de2617565ef..0c49ccf276b2b2 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 e38520f00e45b6..687a0e87d4c68e 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 f795b32d78b8e8..2d4c461cc2c2e4 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 21dc681951b212..db53df789ac69b 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 177fd1f153155c..b7ba1e87b2f001 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 4da4fb21fbed5f..abf64906e02539 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 02b64157686c19..0f981f3d07610f 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 0e3bb235c3d9f5..ff09d8d4fc5ab6 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.6.3", "@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 ca4714779d39e1..4b2758516fc260 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 49a255d60f29e7..5a807b4b090c11 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 6c10539c6b7d3b..effa6c77d76148 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 66f68f815edaca..2ddccae2fada66 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 dc6e6d56766510..097a549187429f 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 656944d85b254d..0170cb53e19df5 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 bf9b4235e94445..e31094d96f3d4c 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 4be46899cff67b..87825350b4e98f 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 cb8671ba37a6c3..7dc5f3655fca03 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 cb279b2cc4c8f9..c4daaf5d7f3072 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 d3ba506b865a47..165ef98be91d41 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 95296346ad8353..06869089598a92 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 483534e0c145be..715f5b883139f9 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 f81903d76547a3..3b3b8da51a9075 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 ffbdabadd03f7a..eccc9d013176cf 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 3b16bed92df976..4a6d86a0dfba61 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 a31c9fe0c3ba17..91b2b1239fc550 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 c037ed733549e6..7521e4a4bee869 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 376f823267ebe6..07675bb0a6819e 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 3f2c31a7c0e5cf..2d27ca7c8a29b4 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/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts index 5636dcadb444e2..44490228490cc3 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts @@ -25,6 +25,7 @@ const createRegistryMock = (): jest.Mocked< const mock = { registerType: jest.fn(), getType: jest.fn(), + getVisibleTypes: jest.fn(), getAllTypes: jest.fn(), getImportableAndExportableTypes: jest.fn(), isNamespaceAgnostic: jest.fn(), @@ -35,6 +36,7 @@ const createRegistryMock = (): jest.Mocked< isImportableAndExportable: jest.fn(), }; + mock.getVisibleTypes.mockReturnValue([]); mock.getAllTypes.mockReturnValue([]); mock.getImportableAndExportableTypes.mockReturnValue([]); mock.getIndex.mockReturnValue('.kibana-test'); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts index e0f4d6fa28e507..25c94324c8f01e 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -99,10 +99,37 @@ describe('SavedObjectTypeRegistry', () => { }); }); + describe('#getVisibleTypes', () => { + it('returns only visible registered types', () => { + const typeA = createType({ name: 'typeA', hidden: false }); + const typeB = createType({ name: 'typeB', hidden: true }); + const typeC = createType({ name: 'typeC', hidden: false }); + registry.registerType(typeA); + registry.registerType(typeB); + registry.registerType(typeC); + + const registered = registry.getVisibleTypes(); + expect(registered.length).toEqual(2); + expect(registered).toContainEqual(typeA); + expect(registered).toContainEqual(typeC); + }); + + it('does not mutate the registered types when altering the list', () => { + registry.registerType(createType({ name: 'typeA', hidden: false })); + registry.registerType(createType({ name: 'typeB', hidden: true })); + registry.registerType(createType({ name: 'typeC', hidden: false })); + + const types = registry.getVisibleTypes(); + types.splice(0, 2); + + expect(registry.getVisibleTypes().length).toEqual(2); + }); + }); + describe('#getAllTypes', () => { it('returns all registered types', () => { const typeA = createType({ name: 'typeA' }); - const typeB = createType({ name: 'typeB' }); + const typeB = createType({ name: 'typeB', hidden: true }); const typeC = createType({ name: 'typeC' }); registry.registerType(typeA); registry.registerType(typeB); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index 99262d7a31e215..d0035294226ea9 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -54,7 +54,18 @@ export class SavedObjectTypeRegistry { } /** - * Return all {@link SavedObjectsType | types} currently registered. + * Returns all visible {@link SavedObjectsType | types}. + * + * A visible type is a type that doesn't explicitly define `hidden=true` during registration. + */ + public getVisibleTypes() { + return [...this.types.values()].filter((type) => !this.isHidden(type.name)); + } + + /** + * Return all {@link SavedObjectsType | types} currently registered, including the hidden ones. + * + * To only get the visible types (which is the most common use case), use `getVisibleTypes` instead. */ public getAllTypes() { return [...this.types.values()]; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index f24195c0f295e3..880b71e164b5b8 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 9cc5a8a386b0b6..cb413be2c19b89 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) @@ -2468,6 +2472,7 @@ export class SavedObjectTypeRegistry { getImportableAndExportableTypes(): SavedObjectsType[]; getIndex(type: string): string | undefined; getType(type: string): SavedObjectsType | undefined; + getVisibleTypes(): SavedObjectsType[]; isHidden(type: string): boolean; isImportableAndExportable(type: string): boolean; isMultiNamespace(type: string): boolean; diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 26704f46a509c4..452d1954b6e235 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -25,10 +25,7 @@ export const uiSettingsType: SavedObjectsType = { hidden: false, namespaceType: 'single', mappings: { - // we don't want to allow `true` in the public `SavedObjectsTypeMappingDefinition` type, however - // this is needed for the config that is kinda a special type. To avoid adding additional internal types - // just for this, we hardcast to any here. - dynamic: true as any, + dynamic: false, properties: { buildNum: { type: 'keyword', diff --git a/src/core/utils/deep_freeze.test.ts b/src/core/utils/deep_freeze.test.ts index 58aa9c9b8c92ba..48f890160d05d9 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 cce8fd3c2e62d9..3a493539f6743a 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 2fd1d5cbe8d48d..746bccc3d718a4 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 2a65839f85ac31..95056d9f0d8d7e 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 00000000000000..89936d6fc4b0ee --- /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 4cb6c2892c4f2a..b8c9acd6fc49d6 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 d3cf31fc0f427f..0b67dac3074738 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 bc3a6265cc5823..cec80dd547a53c 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 7075bcf55adf56..68058043477d0a 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 0e576a88ab36a2..fde1e54b0661df 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 c588b941112d1f..82e00a99c6cfdb 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 fc4ff512e2bd58..d76b2a2aa9364c 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 4f8cee2651a9f1..20281d8479ab4d 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 f075dff466793c..6b7ccaed25d492 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 e9810a747c8c78..7de0c8fc15f943 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 8020cef8d84927..d8dbf8d6dc8856 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 b5501982cec095..602b221b7d14d6 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 879fab206b99df..ae042310fd464d 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 f3fd2fde8f2c56..2102b02194bc83 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 577ee984e05c69..3750e15c000e71 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 ebf000d160b546..cf2a7859a505d8 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 b1999eb4b483c4..087e1669253278 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 17da5ffca12421..db1ec425f2ce5c 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 a852fba4a1c5a4..7400d84ea2ce7d 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 9bc1d67dd5857b..8a80cbef1a9c57 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 019941534cadd3..ed2657ed3f6ee5 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 2c0a2d84be2c00..536a6dc581db6c 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/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 63839b9d0f1d77..185c8807ae8b5f 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -34,8 +34,8 @@ export function savedObjectsMixin(kbnServer, server) { const typeRegistry = kbnServer.newPlatform.start.core.savedObjects.getTypeRegistry(); const mappings = migrator.getActiveMappings(); const allTypes = typeRegistry.getAllTypes().map((t) => t.name); + const visibleTypes = typeRegistry.getVisibleTypes().map((t) => t.name); const schema = new SavedObjectsSchema(convertTypesToLegacySchema(typeRegistry.getAllTypes())); - const visibleTypes = allTypes.filter((type) => !schema.isHiddenType(type)); server.decorate('server', 'kibanaMigrator', migrator); diff --git a/src/legacy/server/status/server_status.js b/src/legacy/server/status/server_status.js index 3ee4d37d0b8239..81d07de55faaf4 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 bf05f45ff856c8..4a34684571c3cd 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 1dc8a71afb1934..464c03d98b83f0 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 a8abbba9df433b..df96a58a6e99fa 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 79ef5e8c183da7..b9a427b8da7adc 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 51bde8b8605ac0..eb47a3e9ace709 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 93428e9f8fa4ed..d91834adb4a79e 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 00000000000000..d5a8535f327188 --- /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 74fc54b4102856..522b346b64900b 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 00000000000000..972365e9e9d0ec --- /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 00000000000000..dc99f4d45082ed --- /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 45e5a0704c37bb..b882a2bbe6e5b9 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 ca2e944489a734..bbca051ce31a1d 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 0cfcb91aa94efe..b4b18e086e809f 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 2e9120eb32b7c8..2c58d851879854 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 8b4cc0a7be1cd6..db6f0e5ea9ef16 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 319da67981aa99..31727b7acb7a1a 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 2c9f37afc14c50..e97ca8ac257b4c 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 fe0deac734e6b9..3b9e1501d638d8 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 00000000000000..fb4f941f793447 --- /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 8aa1a4f2368ac3..7fecb862a3c65f 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 fca503e387ea25..52bc78dfec7dfa 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 e1e71573caa3a5..2d0c4de8832188 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 377e739a0c59a6..ebcc2a35b61119 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 b7cc8f2f4b72fb..06823a981af46f 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 f37b3ac0cca9c5..d31507626146e1 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 b770638a61ff7e..b26a2234333331 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 79a332624e5e1e..412fda16d45b65 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 38be0d8a7e4c99..b893218f4967ca 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 a237fe5dd59d6b..037f4b1b27c500 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 aafb234b0f4466..0e3b6a345836b5 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 22aae8da030d48..88fe195bcbf2be 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 28a971794d403d..38592e66bd8be3 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 272f63322ffaa4..a16fb1dadfbcf0 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 ccd3b6b1c0a82a..ce990e62a22843 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 96210358c05e46..26af13b4410fe6 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 57fe4acf081453..e4a98ffac7a554 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 58477d28f9081e..a321bc7959c5c8 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 79116a57869d35..a6928c0608bd28 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 1b060c186db973..5ecd57d670ae83 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 e3b6725ce74496..72d3ffe6b2322d 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 4f7945d6dd6017..1e8356a1ef1003 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 75e169b79f320f..452d68aa923945 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 ae9d1c79219551..261977b8596591 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 990d5883594423..4441155ad9218c 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 c318a0f0c2c3d9..c355a7397797c9 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 89aec6e55e81ba..404f27b38992c3 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 a6c4a1133a2ed3..c9ab9df920e16d 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 2229601994496d..2b8f9ad48a34b5 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 26f07a12067ce5..9e4308d6fd559e 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 9325485bce75d6..74a942b51583df 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 99b49b401a8b8e..0bad47d9889f0c 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 26f1a185ada3a7..1702441aa4ca11 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 c194687b7c3bfa..c1aa2efe46998a 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 bde550c660a323..dab11ad0ce29a1 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 65df6e26a25b30..be8e9b13e7cffc 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 8f1d02c5ffd547..345dd3b32691ec 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 f19611bc1d5264..670b40e7d94722 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 60a49a4bd50f4a..eaf6ddc9afc35f 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 432a763bfd48c9..723001297e8f25 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 d2d5a4b0692186..41457a01e0c97d 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 8650f5920e5202..de49e9ab6f66e6 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 121bb29f6f8edb..f3efeb028665b1 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 4052c0b3901556..cb17ef07a930ba 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 018fcb365b5850..ed9bc5e0462f15 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 12197c85f4a961..017f646258c012 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 47da7e59af5e06..8dc8b786fcfcd2 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 00d866e6f2b3ed..25d3a3ea90a4ab 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/search/tabify/get_columns.test.ts b/src/plugins/data/public/search/tabify/get_columns.test.ts index 0c5551d95690f2..35f0181f633026 100644 --- a/src/plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/plugins/data/public/search/tabify/get_columns.test.ts @@ -161,4 +161,20 @@ describe('get columns', () => { 'Sum of @timestamp', ]); }); + + test('should not fail if there is no field for date histogram agg', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + { type: 'sum', schema: 'metric', params: { field: '@timestamp' } }, + ]).aggs, + false + ); + + expect(columns.map((c) => c.name)).toEqual(['', 'Sum of @timestamp']); + }); }); diff --git a/src/plugins/data/public/search/tabify/get_columns.ts b/src/plugins/data/public/search/tabify/get_columns.ts index 8c538288d2feaf..8e907d4b0cb883 100644 --- a/src/plugins/data/public/search/tabify/get_columns.ts +++ b/src/plugins/data/public/search/tabify/get_columns.ts @@ -22,10 +22,17 @@ import { IAggConfig } from '../aggs'; import { TabbedAggColumn } from './types'; const getColumn = (agg: IAggConfig, i: number): TabbedAggColumn => { + let name = ''; + try { + name = agg.makeLabel(); + } catch (e) { + // skip the case when makeLabel throws an error (e.x. no appropriate field for an aggregation) + } + return { aggConfig: agg, id: `col-${i}-${agg.id}`, - name: agg.makeLabel(), + name, }; }; 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 43dba150bf8d44..fdd952e2207d93 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 3fb7f198d5466a..b97e0e33f2400b 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 > { - 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 a0df7604f23aae..f8b7e4f4809112 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 502364cdcba327..b4b86b73a5f4a5 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 a01d34dbe9df6c..2e408d7569be56 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts @@ -46,7 +46,7 @@ export async function resolveTimePattern(callCluster: LegacyAPICaller, timePatte [] ) .sortBy((indexName: string) => indexName) - .uniq(true) + .sortedUniq() .map((indexName) => { const parsed = moment(indexName, timePattern, true); if (!parsed.isValid()) { @@ -65,7 +65,7 @@ export async function resolveTimePattern(callCluster: LegacyAPICaller, timePatte isMatch: indexName === parsed.format(timePattern), }; }) - .sortByOrder(['valid', 'order'], ['desc', 'desc']) + .orderBy(['valid', 'order'], ['desc', 'desc']) .value(); return { diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts index 37819a13b65187..768041a376ad1d 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 902cf2988f429f..44d2813f6e3e81 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -54,5 +54,5 @@ export const indexPatternSavedObjectType: SavedObjectsType = { typeMeta: { type: 'keyword' }, }, }, - migrations: indexPatternSavedObjectTypeMigrations, + migrations: indexPatternSavedObjectTypeMigrations as any, }; diff --git a/src/plugins/data/server/saved_objects/search.ts b/src/plugins/data/server/saved_objects/search.ts index 437c83f67bf5d0..16caaf05a0fc60 100644 --- a/src/plugins/data/server/saved_objects/search.ts +++ b/src/plugins/data/server/saved_objects/search.ts @@ -56,5 +56,5 @@ export const searchSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: searchSavedObjectTypeMigrations, + migrations: searchSavedObjectTypeMigrations as any, }; diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts index 2e37cd1255cee5..9bba429f8d71be 100644 --- a/src/plugins/data/server/saved_objects/search_migrations.ts +++ b/src/plugins/data/server/saved_objects/search_migrations.ts @@ -22,7 +22,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../common'; const migrateMatchAllQuery: SavedObjectMigrationFn = (doc) => { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { let searchSource: any; @@ -122,7 +122,7 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; export const searchSavedObjectTypeMigrations = { - '6.7.2': flow>(migrateMatchAllQuery), - '7.0.0': flow>(setNewReferences), - '7.4.0': flow>(migrateSearchSortToNestedArray), + '6.7.2': flow(migrateMatchAllQuery), + '7.0.0': flow(setNewReferences), + '7.4.0': flow(migrateSearchSortToNestedArray), }; diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover/public/application/angular/context/query/actions.js index 0e057e0a715c41..32fc2873d7f2a2 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 9afe5e48bc5b82..4c39c8bb255422 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 ({ 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/expressions/common/expression_functions/specs/kibana_context.ts b/src/plugins/expressions/common/expression_functions/specs/kibana_context.ts index b8be273d7bbd32..2b7d1b8ed9d760 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 c113765f8e7e72..5cd53df663e1d4 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 7f2f3c37c587c1..e226f3b124eed0 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 9428d7db1d9d0d..f957f10a9aeba1 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -19,6 +19,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; +import { defaults } from 'lodash'; import { Adapters } from '../../inspector/public'; import { IExpressionLoaderParams } from './types'; import { ExpressionAstExpression } from '../common'; @@ -168,7 +169,7 @@ export class ExpressionLoader { } if (params.searchContext) { - this.params.searchContext = _.defaults( + this.params.searchContext = defaults( {}, params.searchContext, this.params.searchContext || {} diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index 52cd5b0c3f5bd3..5ab9c695caaa04 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 4eff5112c0c072..03ed6c5520decd 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 70af6b5b51d182..af10d1b77b16d3 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 c0e0153c6053ee..5a970cc6cff386 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_legacy/public/forward_app/forward_app.ts b/src/plugins/kibana_legacy/public/forward_app/forward_app.ts index 89018df1ca7e17..b425091dfbcd9b 100644 --- a/src/plugins/kibana_legacy/public/forward_app/forward_app.ts +++ b/src/plugins/kibana_legacy/public/forward_app/forward_app.ts @@ -20,9 +20,12 @@ import { App, AppMountParameters, CoreSetup } from 'kibana/public'; import { AppNavLinkStatus } from '../../../../core/public'; import { navigateToLegacyKibanaUrl } from './navigate_to_legacy_kibana_url'; -import { ForwardDefinition } from '../plugin'; +import { ForwardDefinition, KibanaLegacyStart } from '../plugin'; -export const createLegacyUrlForwardApp = (core: CoreSetup, forwards: ForwardDefinition[]): App => ({ +export const createLegacyUrlForwardApp = ( + core: CoreSetup<{}, KibanaLegacyStart>, + forwards: ForwardDefinition[] +): App => ({ id: 'kibana', chromeless: true, title: 'Legacy URL migration', @@ -31,7 +34,8 @@ export const createLegacyUrlForwardApp = (core: CoreSetup, forwards: ForwardDefi const hash = params.history.location.hash.substr(1); if (!hash) { - core.fatalErrors.add('Could not forward URL'); + const [, , kibanaLegacyStart] = await core.getStartServices(); + kibanaLegacyStart.navigateToDefaultApp(); } const [ @@ -44,7 +48,8 @@ export const createLegacyUrlForwardApp = (core: CoreSetup, forwards: ForwardDefi const result = await navigateToLegacyKibanaUrl(hash, forwards, basePath, application); if (!result.navigated) { - core.fatalErrors.add('Could not forward URL'); + const [, , kibanaLegacyStart] = await core.getStartServices(); + kibanaLegacyStart.navigateToDefaultApp(); } return () => {}; 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 87fdf0730c8807..2fa1debf51b5c3 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_react/public/validated_range/validated_dual_range.tsx b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index 63b9b48ec809eb..45592c8a703af1 100644 --- a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { EuiFormRow, EuiDualRange } from '@elastic/eui'; import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; @@ -32,7 +32,7 @@ export type ValueMember = EuiDualRangeProps['value'][0]; interface Props extends Omit { value?: Value; allowEmptyRange?: boolean; - label?: string; + label?: string | ReactNode; formRowDisplay?: EuiFormRowDisplayKeys; onChange?: (val: [string, string]) => void; min?: number; 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 a0de79da565e62..8d6a2d110efe0f 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,10 +35,12 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe hidden: false, namespaceType: 'agnostic', mappings: { + dynamic: false, properties: { - appId: { type: 'keyword' }, - numberOfClicks: { type: 'long' }, - minutesOnScreen: { type: 'float' }, + // 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' }, }, }, }); @@ -48,11 +50,13 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe hidden: false, namespaceType: 'agnostic', mappings: { + dynamic: false, properties: { timestamp: { type: 'date' }, - appId: { type: 'keyword' }, - numberOfClicks: { type: 'long' }, - minutesOnScreen: { type: 'float' }, + // 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 fb60f0ceff10f2..fe8cf12d0d6f20 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 3a05ce59f5d134..1c5642f9b75b7e 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 5f424751dd58ea..dcfb44d8a5224f 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 75692777f08bbb..dbbea4012aba9d 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/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index d048c8f5e94279..42259d2e5187ce 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -47,4 +47,8 @@ export { getLocalLicense, getLocalStats, TelemetryLocalStats, + DATA_TELEMETRY_ID, + DataTelemetryIndex, + DataTelemetryPayload, + buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index e78b92498e6e78..8541745faea3b6 100644 --- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -179,23 +179,36 @@ describe('get_local_stats', () => { describe('handleLocalStats', () => { it('returns expected object without xpack and kibana data', () => { - const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); expect(result.version).to.be('2.3.4'); expect(result.collection).to.be('local'); expect(result.license).to.be(undefined); - expect(result.stack_stats).to.eql({ kibana: undefined }); + expect(result.stack_stats).to.eql({ kibana: undefined, data: undefined }); }); it('returns expected object with xpack', () => { - const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); const { stack_stats: stack, ...cluster } = result; expect(cluster.collection).to.be(combinedStatsResult.collection); expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid); expect(cluster.cluster_name).to.be(combinedStatsResult.cluster_name); expect(stack.kibana).to.be(undefined); // not mocked for this test + expect(stack.data).to.be(undefined); // not mocked for this test expect(cluster.version).to.eql(combinedStatsResult.version); expect(cluster.cluster_stats).to.eql(combinedStatsResult.cluster_stats); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts new file mode 100644 index 00000000000000..2d0864b1cb75f8 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DATA_TELEMETRY_ID = 'data'; + +export const DATA_KNOWN_TYPES = ['logs', 'traces', 'metrics'] as const; + +export type DataTelemetryType = typeof DATA_KNOWN_TYPES[number]; + +export type DataPatternName = typeof DATA_DATASETS_INDEX_PATTERNS[number]['patternName']; + +// TODO: Ideally this list should be updated from an external public URL (similar to the newsfeed) +// But it's good to have a minimum list shipped with the build. +export const DATA_DATASETS_INDEX_PATTERNS = [ + // Enterprise Search - Elastic + { pattern: '.ent-search-*', patternName: 'enterprise-search' }, + { pattern: '.app-search-*', patternName: 'app-search' }, + // Enterprise Search - 3rd party + { pattern: '*magento2*', patternName: 'magento2' }, + { pattern: '*magento*', patternName: 'magento' }, + { pattern: '*shopify*', patternName: 'shopify' }, + { pattern: '*wordpress*', patternName: 'wordpress' }, + // { pattern: '*wp*', patternName: 'wordpress' }, // TODO: Too vague? + { pattern: '*drupal*', patternName: 'drupal' }, + { pattern: '*joomla*', patternName: 'joomla' }, + { pattern: '*search*', patternName: 'search' }, // TODO: Too vague? + // { pattern: '*wix*', patternName: 'wix' }, // TODO: Too vague? + { pattern: '*sharepoint*', patternName: 'sharepoint' }, + { pattern: '*squarespace*', patternName: 'squarespace' }, + // { pattern: '*aem*', patternName: 'aem' }, // TODO: Too vague? + { pattern: '*sitecore*', patternName: 'sitecore' }, + { pattern: '*weebly*', patternName: 'weebly' }, + { pattern: '*acquia*', patternName: 'acquia' }, + + // Observability - Elastic + { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' }, + { pattern: 'metricbeat-*', patternName: 'metricbeat', shipper: 'metricbeat' }, + { pattern: 'apm-*', patternName: 'apm', shipper: 'apm' }, + { pattern: 'functionbeat-*', patternName: 'functionbeat', shipper: 'functionbeat' }, + { pattern: 'heartbeat-*', patternName: 'heartbeat', shipper: 'heartbeat' }, + { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' }, + // Observability - 3rd party + { pattern: 'fluentd*', patternName: 'fluentd' }, + { pattern: 'telegraf*', patternName: 'telegraf' }, + { pattern: 'prometheusbeat*', patternName: 'prometheusbeat' }, + { pattern: 'fluentbit*', patternName: 'fluentbit' }, + { pattern: '*nginx*', patternName: 'nginx' }, + { pattern: '*apache*', patternName: 'apache' }, // Already in Security (keeping it in here for documentation) + // { pattern: '*logs*', patternName: 'third-party-logs' }, Disabled for now + + // Security - Elastic + { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' }, + { pattern: 'endgame-*', patternName: 'endgame', shipper: 'endgame' }, + { pattern: 'logs-endpoint.*', patternName: 'logs-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case + { pattern: 'metrics-endpoint.*', patternName: 'metrics-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case + { pattern: '.siem-signals-*', patternName: 'siem-signals' }, + { pattern: 'auditbeat-*', patternName: 'auditbeat', shipper: 'auditbeat' }, + { pattern: 'winlogbeat-*', patternName: 'winlogbeat', shipper: 'winlogbeat' }, + { pattern: 'packetbeat-*', patternName: 'packetbeat', shipper: 'packetbeat' }, + { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' }, + // Security - 3rd party + { pattern: '*apache*', patternName: 'apache' }, // Already in Observability (keeping it in here for documentation) + { pattern: '*tomcat*', patternName: 'tomcat' }, + { pattern: '*artifactory*', patternName: 'artifactory' }, + { pattern: '*aruba*', patternName: 'aruba' }, + { pattern: '*barracuda*', patternName: 'barracuda' }, + { pattern: '*bluecoat*', patternName: 'bluecoat' }, + { pattern: 'arcsight-*', patternName: 'arcsight', shipper: 'arcsight' }, + // { pattern: '*cef*', patternName: 'cef' }, // Disabled because it's too vague + { pattern: '*checkpoint*', patternName: 'checkpoint' }, + { pattern: '*cisco*', patternName: 'cisco' }, + { pattern: '*citrix*', patternName: 'citrix' }, + { pattern: '*cyberark*', patternName: 'cyberark' }, + { pattern: '*cylance*', patternName: 'cylance' }, + { pattern: '*fireeye*', patternName: 'fireeye' }, + { pattern: '*fortinet*', patternName: 'fortinet' }, + { pattern: '*infoblox*', patternName: 'infoblox' }, + { pattern: '*kaspersky*', patternName: 'kaspersky' }, + { pattern: '*mcafee*', patternName: 'mcafee' }, + // paloaltonetworks + { pattern: '*paloaltonetworks*', patternName: 'paloaltonetworks' }, + { pattern: 'pan-*', patternName: 'paloaltonetworks' }, + { pattern: 'pan_*', patternName: 'paloaltonetworks' }, + { pattern: 'pan.*', patternName: 'paloaltonetworks' }, + + // rsa + { pattern: 'rsa.*', patternName: 'rsa' }, + { pattern: 'rsa-*', patternName: 'rsa' }, + { pattern: 'rsa_*', patternName: 'rsa' }, + + // snort + { pattern: 'snort-*', patternName: 'snort' }, + { pattern: 'logstash-snort*', patternName: 'snort' }, + + { pattern: '*sonicwall*', patternName: 'sonicwall' }, + { pattern: '*sophos*', patternName: 'sophos' }, + + // squid + { pattern: 'squid-*', patternName: 'squid' }, + { pattern: 'squid_*', patternName: 'squid' }, + { pattern: 'squid.*', patternName: 'squid' }, + + { pattern: '*symantec*', patternName: 'symantec' }, + { pattern: '*tippingpoint*', patternName: 'tippingpoint' }, + { pattern: '*trendmicro*', patternName: 'trendmicro' }, + { pattern: '*tripwire*', patternName: 'tripwire' }, + { pattern: '*zscaler*', patternName: 'zscaler' }, + { pattern: '*zeek*', patternName: 'zeek' }, + { pattern: '*sigma_doc*', patternName: 'sigma_doc' }, + // { pattern: '*bro*', patternName: 'bro' }, // Disabled because it's too vague + { pattern: 'ecs-corelight*', patternName: 'ecs-corelight' }, + { pattern: '*suricata*', patternName: 'suricata' }, + // { pattern: '*fsf*', patternName: 'fsf' }, // Disabled because it's too vague + { pattern: '*wazuh*', patternName: 'wazuh' }, +] as const; + +// Get the unique list of index patterns (some are duplicated for documentation purposes) +export const DATA_DATASETS_INDEX_PATTERNS_UNIQUE = DATA_DATASETS_INDEX_PATTERNS.filter( + (entry, index, array) => !array.slice(0, index).find(({ pattern }) => entry.pattern === pattern) +); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts new file mode 100644 index 00000000000000..8bffc5d012a741 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -0,0 +1,251 @@ +/* + * 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 { buildDataTelemetryPayload, getDataTelemetry } from './get_data_telemetry'; +import { DATA_DATASETS_INDEX_PATTERNS, DATA_DATASETS_INDEX_PATTERNS_UNIQUE } from './constants'; + +describe('get_data_telemetry', () => { + describe('DATA_DATASETS_INDEX_PATTERNS', () => { + DATA_DATASETS_INDEX_PATTERNS.forEach((entry, index, array) => { + describe(`Pattern ${entry.pattern}`, () => { + test('there should only be one in DATA_DATASETS_INDEX_PATTERNS_UNIQUE', () => { + expect( + DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => pattern === entry.pattern) + ).toHaveLength(1); + }); + + // This test is to make us sure that we don't update one of the duplicated entries and forget about any other repeated ones + test('when a document is duplicated, the duplicates should be identical', () => { + array.slice(0, index).forEach((previousEntry) => { + if (entry.pattern === previousEntry.pattern) { + expect(entry).toStrictEqual(previousEntry); + } + }); + }); + }); + }); + }); + + describe('buildDataTelemetryPayload', () => { + test('return the base object when no indices provided', () => { + expect(buildDataTelemetryPayload([])).toStrictEqual([]); + }); + + test('return the base object when no matching indices provided', () => { + expect( + buildDataTelemetryPayload([ + { name: 'no__way__this__can_match_anything', sizeInBytes: 10 }, + { name: '.kibana-event-log-8.0.0' }, + ]) + ).toStrictEqual([]); + }); + + test('matches some indices and puts them in their own category', () => { + expect( + buildDataTelemetryPayload([ + // APM Indices have known shipper (so we can infer the datasetType from mapping constant) + { name: 'apm-7.7.0-error-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-metric-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-onboarding-2020.05.17', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-profile-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-span-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-transaction-000001', shipper: 'apm', isECS: true }, + // Packetbeat indices with known shipper (we can infer datasetType from mapping constant) + { name: 'packetbeat-7.7.0-2020.06.11-000001', shipper: 'packetbeat', isECS: true }, + // Matching patterns from the list => known datasetName but the rest is unknown + { name: 'filebeat-12314', docCount: 100, sizeInBytes: 10 }, + { name: 'metricbeat-1234', docCount: 100, sizeInBytes: 10, isECS: false }, + { name: '.app-search-1234', docCount: 0 }, + { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name + // New Indexing strategy: everything can be inferred from the constant_keyword values + { + name: 'logs-nginx.access-default-000001', + datasetName: 'nginx.access', + datasetType: 'logs', + shipper: 'filebeat', + isECS: true, + docCount: 1000, + sizeInBytes: 1000, + }, + { + name: 'logs-nginx.access-default-000002', + datasetName: 'nginx.access', + datasetType: 'logs', + shipper: 'filebeat', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + ]) + ).toStrictEqual([ + { + shipper: 'apm', + index_count: 6, + ecs_index_count: 6, + }, + { + shipper: 'packetbeat', + index_count: 1, + ecs_index_count: 1, + }, + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + { + pattern_name: 'metricbeat', + shipper: 'metricbeat', + index_count: 1, + ecs_index_count: 0, + doc_count: 100, + size_in_bytes: 10, + }, + { + pattern_name: 'app-search', + index_count: 1, + doc_count: 0, + }, + { + pattern_name: 'logs-endpoint', + shipper: 'endpoint', + index_count: 1, + doc_count: 0, + }, + { + dataset: { name: 'nginx.access', type: 'logs' }, + shipper: 'filebeat', + index_count: 2, + ecs_index_count: 2, + doc_count: 2000, + size_in_bytes: 1060, + }, + ]); + }); + }); + + describe('getDataTelemetry', () => { + test('it returns the base payload (all 0s) because no indices are found', async () => { + const callCluster = mockCallCluster(); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]); + }); + + test('can only see the index mappings, but not the stats', async () => { + const callCluster = mockCallCluster(['filebeat-12314']); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + ecs_index_count: 0, + }, + ]); + }); + + test('can see the mappings and the stats', async () => { + const callCluster = mockCallCluster( + ['filebeat-12314'], + { isECS: true }, + { + indices: { + 'filebeat-12314': { total: { docs: { count: 100 }, store: { size_in_bytes: 10 } } }, + }, + } + ); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + ecs_index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + ]); + }); + + test('find an index that does not match any index pattern but has mappings metadata', async () => { + const callCluster = mockCallCluster( + ['cannot_match_anything'], + { isECS: true, datasetType: 'traces', shipper: 'my-beat' }, + { + indices: { + cannot_match_anything: { + total: { docs: { count: 100 }, store: { size_in_bytes: 10 } }, + }, + }, + } + ); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + dataset: { name: undefined, type: 'traces' }, + shipper: 'my-beat', + index_count: 1, + ecs_index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + ]); + }); + + test('return empty array when there is an error', async () => { + const callCluster = jest.fn().mockRejectedValue(new Error('Something went terribly wrong')); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]); + }); + }); +}); + +function mockCallCluster( + indicesMappings: string[] = [], + { isECS = false, datasetName = '', datasetType = '', shipper = '' } = {}, + indexStats: any = {} +) { + return jest.fn().mockImplementation(async (method: string, opts: any) => { + if (method === 'indices.getMapping') { + return Object.fromEntries( + indicesMappings.map((index) => [ + index, + { + mappings: { + ...(shipper && { _meta: { beat: shipper } }), + properties: { + ...(isECS && { ecs: { properties: { version: { type: 'keyword' } } } }), + ...((datasetType || datasetName) && { + dataset: { + properties: { + ...(datasetName && { + name: { type: 'constant_keyword', value: datasetName }, + }), + ...(datasetType && { + type: { type: 'constant_keyword', value: datasetType }, + }), + }, + }, + }), + }, + }, + }, + ]) + ); + } + return indexStats; + }); +} diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts new file mode 100644 index 00000000000000..cf906bc5c86cfc --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -0,0 +1,253 @@ +/* + * 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 { LegacyAPICaller } from 'kibana/server'; +import { + DATA_DATASETS_INDEX_PATTERNS_UNIQUE, + DataPatternName, + DataTelemetryType, +} from './constants'; + +export interface DataTelemetryBasePayload { + index_count: number; + ecs_index_count?: number; + doc_count?: number; + size_in_bytes?: number; +} + +export interface DataTelemetryDocument extends DataTelemetryBasePayload { + dataset?: { + name?: string; + type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s + }; + shipper?: string; + pattern_name?: DataPatternName; +} + +export type DataTelemetryPayload = DataTelemetryDocument[]; + +export interface DataTelemetryIndex { + name: string; + datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword + datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword + shipper?: string; // To be obtained from `_meta.beat` if it's set + isECS?: boolean; // Optional because it can't be obtained via Monitoring. + + // The fields below are optional because we might not be able to obtain them if the user does not + // have access to the index. + docCount?: number; + sizeInBytes?: number; +} + +type AtLeastOne }> = Partial & U[keyof U]; + +type DataDescriptor = AtLeastOne<{ + datasetName: string; + datasetType: string; + shipper: string; + patternName: DataPatternName; // When found from the list of the index patterns +}>; + +function findMatchingDescriptors({ + name, + shipper, + datasetName, + datasetType, +}: DataTelemetryIndex): DataDescriptor[] { + // If we already have the data from the indices' mappings... + if ([shipper, datasetName, datasetType].some(Boolean)) { + return [ + { + ...(shipper && { shipper }), + ...(datasetName && { datasetName }), + ...(datasetType && { datasetType }), + } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause + ]; + } + + // Otherwise, try with the list of known index patterns + return DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => { + if (!pattern.startsWith('.') && name.startsWith('.')) { + // avoid system indices caught by very fuzzy index patterns (i.e.: *log* would catch `.kibana-log-...`) + return false; + } + return new RegExp(`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`).test(name); + }); +} + +function increaseCounters( + previousValue: DataTelemetryBasePayload = { index_count: 0 }, + { isECS, docCount, sizeInBytes }: DataTelemetryIndex +) { + return { + ...previousValue, + index_count: previousValue.index_count + 1, + ...(typeof isECS === 'boolean' + ? { + ecs_index_count: (previousValue.ecs_index_count || 0) + (isECS ? 1 : 0), + } + : {}), + ...(typeof docCount === 'number' + ? { doc_count: (previousValue.doc_count || 0) + docCount } + : {}), + ...(typeof sizeInBytes === 'number' + ? { size_in_bytes: (previousValue.size_in_bytes || 0) + sizeInBytes } + : {}), + }; +} + +export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTelemetryPayload { + const startingDotPatternsUntilTheFirstAsterisk = DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map( + ({ pattern }) => pattern.replace(/^\.(.+)\*.*$/g, '.$1') + ).filter(Boolean); + + // Filter out the system indices unless they are required by the patterns + const indexCandidates = indices.filter( + ({ name }) => + !( + name.startsWith('.') && + !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern)) + ) + ); + + const acc = new Map(); + + for (const indexCandidate of indexCandidates) { + const matchingDescriptors = findMatchingDescriptors(indexCandidate); + for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) { + const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`; + acc.set(key, { + ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...(shipper && { shipper }), + ...(patternName && { pattern_name: patternName }), + ...increaseCounters(acc.get(key), indexCandidate), + }); + } + } + + return [...acc.values()]; +} + +interface IndexStats { + indices: { + [indexName: string]: { + total: { + docs: { + count: number; + deleted: number; + }; + store: { + size_in_bytes: number; + }; + }; + }; + }; +} + +interface IndexMappings { + [indexName: string]: { + mappings: { + _meta?: { + beat?: string; + }; + properties: { + dataset?: { + properties: { + name?: { + type: string; + value?: string; + }; + type?: { + type: string; + value?: string; + }; + }; + }; + ecs?: { + properties: { + version?: { + type: string; + }; + }; + }; + }; + }; + }; +} + +export async function getDataTelemetry(callCluster: LegacyAPICaller) { + try { + const index = [ + ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), + '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter} + ]; + const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ + // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value + callCluster('indices.getMapping', { + index: '*', // Request all indices because filter_path already filters out the indices without any of those fields + filterPath: [ + // _meta.beat tells the shipper + '*.mappings._meta.beat', + // Does it have `ecs.version` in the mappings? => It follows the ECS conventions + '*.mappings.properties.ecs.properties.version.type', + + // Disable the fields below because they are still pending to be confirmed: + // https://github.com/elastic/ecs/pull/845 + // TODO: Re-enable when the final fields are confirmed + // // If `dataset.type` is a `constant_keyword`, it can be reported as a type + // '*.mappings.properties.dataset.properties.type.value', + // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset + // '*.mappings.properties.dataset.properties.name.value', + ], + }), + // GET /_stats/docs,store?level=indices&filter_path=indices.*.total + callCluster('indices.stats', { + index, + level: 'indices', + metric: ['docs', 'store'], + filterPath: ['indices.*.total'], + }), + ]); + + const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); + const indices = indexNames.map((name) => { + const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type; + const shipper = indexMappings[name]?.mappings?._meta?.beat; + const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value; + const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value; + + const stats = (indexStats?.indices || {})[name]; + if (stats) { + return { + name, + datasetName, + datasetType, + shipper, + isECS, + docCount: stats.total?.docs?.count, + sizeInBytes: stats.total?.store?.size_in_bytes, + }; + } + return { name, datasetName, datasetType, shipper, isECS }; + }); + return buildDataTelemetryPayload(indices); + } catch (e) { + return []; + } +} diff --git a/src/setup_node_env/harden.js b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts similarity index 80% rename from src/setup_node_env/harden.js rename to src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index dead3db1d60b4d..d056d1c9f299f3 100644 --- a/src/setup_node_env/harden.js +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -17,8 +17,11 @@ * under the License. */ -var hook = require('require-in-the-middle'); +export { DATA_TELEMETRY_ID } from './constants'; -hook(['child_process'], function (exports, name) { - return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require -}); +export { + DataTelemetryIndex, + DataTelemetryPayload, + getDataTelemetry, + buildDataTelemetryPayload, +} from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index b42edde2f55ca2..4d4031bb428baf 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -25,6 +25,7 @@ import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; import { getNodesUsage } from './get_nodes_usage'; +import { getDataTelemetry, DATA_TELEMETRY_ID, DataTelemetryPayload } from './get_data_telemetry'; /** * Handle the separate local calls by combining them into a single object response that looks like the @@ -39,6 +40,7 @@ export function handleLocalStats( { cluster_name, cluster_uuid, version }: ESClusterInfo, { _nodes, cluster_name: clusterName, ...clusterStats }: any, kibana: KibanaUsageStats, + dataTelemetry: DataTelemetryPayload, context: StatsCollectionContext ) { return { @@ -49,6 +51,7 @@ export function handleLocalStats( cluster_stats: clusterStats, collection: 'local', stack_stats: { + [DATA_TELEMETRY_ID]: dataTelemetry, kibana: handleKibanaStats(context, kibana), }, }; @@ -68,11 +71,12 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( return await Promise.all( clustersDetails.map(async (clustersDetail) => { - const [clusterInfo, clusterStats, nodesUsage, kibana] = await Promise.all([ + const [clusterInfo, clusterStats, nodesUsage, kibana, dataTelemetry] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) getNodesUsage(callCluster), // nodes_usage info getKibana(usageCollection, callCluster), + getDataTelemetry(callCluster), ]); return handleLocalStats( clusterInfo, @@ -81,6 +85,7 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( nodes: { ...clusterStats.nodes, usage: nodesUsage }, }, kibana, + dataTelemetry, context ); }) diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 377ddab7b877ce..40cbf0e4caa1d9 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -17,6 +17,12 @@ * under the License. */ +export { + DATA_TELEMETRY_ID, + DataTelemetryIndex, + DataTelemetryPayload, + buildDataTelemetryPayload, +} from './get_data_telemetry'; export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; export { getLocalLicense } from './get_local_license'; export { getClusterUuids } from './get_cluster_stats'; diff --git a/src/plugins/tile_map/public/index.scss b/src/plugins/tile_map/public/index.scss index 4ce500b2da4d22..f4b86b0c31190b 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_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index 3030601236687d..4cde33b8fbc314 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -152,7 +152,7 @@ function DefaultEditorAggGroup({ {bucketsError && ( <> - {bucketsError} + {bucketsError} )} 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 45abbf8d2b2dd3..ef2f937c8547cf 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'); } @@ -111,7 +111,11 @@ function getAggParamsToRender({ const aggType = agg.type.type; const aggName = agg.type.name; const aggParams = get(aggParamsMap, [aggType, aggName], {}); - paramEditor = get(aggParams, param.name) || get(aggParamsMap, ['common', param.type]); + paramEditor = get(aggParams, param.name); + } + + if (!paramEditor) { + paramEditor = get(aggParamsMap, ['common', param.type]); } // show params with an editor component 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 a0bc0d78a28895..37e95f2419b458 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 6eaef3050029af..a3998cbd5954be 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 9a9933b5e1e833..04d0df27927fa7 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 0d21eb04c12b2a..f6354027ab01b8 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/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx index 361eeba9abdbf8..fc79ba703c2b4a 100644 --- a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -45,9 +45,10 @@ function SubMetricParamEditor({ defaultMessage: 'Bucket', }); const type = aggParam.name; + const isCustomMetric = type === 'customMetric'; - const aggTitle = type === 'customMetric' ? metricTitle : bucketTitle; - const aggGroup = type === 'customMetric' ? AggGroupNames.Metrics : AggGroupNames.Buckets; + const aggTitle = isCustomMetric ? metricTitle : bucketTitle; + const aggGroup = isCustomMetric ? AggGroupNames.Metrics : AggGroupNames.Buckets; useMount(() => { if (agg.params[type]) { @@ -87,7 +88,7 @@ function SubMetricParamEditor({ setValidity={setValidity} setTouched={setTouched} schemas={schemas} - hideCustomLabel={true} + hideCustomLabel={!isCustomMetric} /> ); 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 4af41f67bc524c..dd9e432fa512e1 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 26567d05e04276..b2c7bcafa15a33 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 54520b85cb5ec9..d95a6252331bfb 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 5e8a463748188c..43858267626124 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_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index c3bc72497007ea..80d53021b7866d 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -26,6 +26,7 @@ import { tableVisResponseHandler } from './table_vis_response_handler'; import tableVisTemplate from './table_vis.html'; import { TableOptions } from './components/table_vis_options_lazy'; import { getTableVisualizationControllerClass } from './vis_controller'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { return { @@ -39,6 +40,9 @@ export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitia defaultMessage: 'Display values in a table', }), visualization: getTableVisualizationControllerClass(core, context), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visConfig: { defaults: { perPage: 10, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 5a8cc3004a3154..023489c6d2e876 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n'; import { Schemas } from '../../vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; // @ts-ignore import { createTagCloudVisualization } from './components/tag_cloud_visualization'; @@ -31,6 +32,9 @@ export const createTagCloudVisTypeDefinition = (deps: TagCloudVisDependencies) = name: 'tagcloud', title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }), icon: 'visTagCloud', + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, description: i18n.translate('visTypeTagCloud.vis.tagCloudDescription', { defaultMessage: 'A group of words, sized according to their importance', }), 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 db29d9112be8e8..860b4e9f2dbde1 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 c02f43818af9c3..7be18a4774d94b 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 06db7bd0e9324c..09518a32864877 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 59adea30730c73..2ee8deb4dd0493 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 9b4fdddc2186e1..11004d2784d3c4 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 536145a6b8dcda..6a2b7c0f5a9f5e 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 83466e263cf2ff..3d53fc8d5bd092 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 d6cb63b7c427b1..699342cff6a796 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 cc13b75fde12d8..1a5d78676fc720 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 7a34b5ec98ff03..412049c89ef2f1 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 409372da24724b..fbae9c5afffe84 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 bc0e368fbdab17..e407636c41567e 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 108eb0c72f19d9..fdaa4dcd8c098e 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 a7ecb4d5b47386..2b9ab08f02edeb 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 bf797bb5aa3437..74d616cffd52d5 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 88ec9fecd904a1..36c5dc708f860f 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 0363ba486a7753..fcb22a9e797078 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 beb691f4b7117e..0638c6e67f5efd 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 3db09bace079f1..c445d456a1703b 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 ddfaf3c1428d9c..612a7a48bade17 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 57ca38168ac27f..d7e6560a8dc971 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 2694732aa381d2..0e13fd7ef68f96 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 fd20ff8b024b35..0f0d99bff6f1c6 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 f6754e5fd9ca43..a9b542af68c9de 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 a9c915fcfb636a..6b1af6044a2c4a 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/area.ts b/src/plugins/vis_type_vislib/public/area.ts index c42962ad50a4b0..ec90fbd1746a15 100644 --- a/src/plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -40,6 +40,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'area', @@ -49,6 +50,9 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: 'Emphasize the quantity beneath a line chart', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'area', 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 7c4f3b3ec8843d..708e8cf15f0296 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/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index ced7a38568ffd0..bd3d02029cb23a 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -28,6 +28,7 @@ import { TimeMarker } from './vislib/visualizations/time_marker'; import { CommonVislibParams, ValueAxis } from './types'; import { VisTypeVislibDependencies } from './plugin'; import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; @@ -48,6 +49,9 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visualization: createVislibVisController(deps), visConfig: { defaults: { diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts index 52242ad11e8f58..8aeeb4ec533abc 100644 --- a/src/plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -39,6 +39,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'histogram', @@ -50,6 +51,9 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'histogram', diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts index a58c15f136431e..702581828e60d0 100644 --- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -37,6 +37,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'horizontal_bar', @@ -48,6 +49,9 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'histogram', diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts index a94fd3f3945ab7..6e9190229114b5 100644 --- a/src/plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -38,6 +38,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'line', @@ -47,6 +48,9 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: 'Emphasize trends', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'line', diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index a68bc5893406f5..1e81dbdde3f685 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -26,6 +26,7 @@ import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; import { CommonVislibParams } from './types'; import { VisTypeVislibDependencies } from './plugin'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export interface PieVisParams extends CommonVislibParams { type: 'pie'; @@ -47,6 +48,9 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ( defaultMessage: 'Compare parts of a whole', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visConfig: { defaults: { type: 'pie', 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 87477332f76e5b..4d4660371eaa49 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 5e78637ef0c025..f04d9d17eeccb4 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 489cb81306b3dc..cf98425c04ce78 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 a2fe4d9249bd06..f7e44ed2787872 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 6b507862fb841f..da046af83a495d 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 e22105d5a086ff..5324dc5318be57 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 0bfcedc5e60555..bafc3199de8964 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 3269f54a621d08..8b7a44d95bb3b0 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 c5fb4761eb9ee0..8a1f80df9f4db1 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 98d384f95a8394..3633063966e17e 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 37f395aab40114..4c50472b9d11a8 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/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 26fdd665192a62..2f9cda32fccdc9 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -377,29 +377,6 @@ export class VisualizeEmbeddable extends Embeddable 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 0c27c3a2c7782f..60945b912e1b3c 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/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 44b76a52b34fef..12b9a49580162c 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -19,11 +19,13 @@ import _ from 'lodash'; import { VisualizationControllerConstructor } from '../types'; +import { TriggerContextMapping } from '../../../ui_actions/public'; export interface BaseVisTypeOptions { name: string; title: string; description?: string; + getSupportedTriggers?: () => Array; icon?: string; image?: string; stage?: 'experimental' | 'beta' | 'production'; @@ -44,6 +46,7 @@ export class BaseVisType { name: string; title: string; description: string; + getSupportedTriggers?: () => Array; icon?: string; image?: string; stage: 'experimental' | 'beta' | 'production'; @@ -77,6 +80,7 @@ export class BaseVisType { this.name = opts.name; this.description = opts.description || ''; + this.getSupportedTriggers = opts.getSupportedTriggers; this.title = opts.title; this.icon = opts.icon; this.image = opts.image; diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index 321f96180fd68e..14c2a9c50ab0eb 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -23,11 +23,13 @@ import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; import { BaseVisType } from './base_vis_type'; // @ts-ignore import { ReactVisType } from './react_vis_type'; +import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisType { name: string; title: string; description?: string; + getSupportedTriggers?: () => Array; visualization: any; isAccessible?: boolean; requestHandler: string | unknown; diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index bc80d549c81e6f..f6d27b54c7c640 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisualizationListItem { editUrl: string; @@ -26,6 +27,7 @@ export interface VisualizationListItem { savedObjectType: string; title: string; description?: string; + getSupportedTriggers?: () => Array; typeTitle: string; image?: string; } @@ -53,6 +55,7 @@ export interface VisTypeAlias { icon: string; promotion?: VisTypeAliasPromotion; description: string; + getSupportedTriggers?: () => Array; stage: 'experimental' | 'beta' | 'production'; appExtensions?: { 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 47757593958d6f..dc6ac4919a4c42 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 27fe722019a27f..74881b9d99ae8c 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 1e05c48ba7daf1..52b7e3ede298b6 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 f5f1a1785bbdf2..94eba5a6d7ce24 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 fb857b2092ee01..6b1ba779605c00 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/harden/child_process.js @@ -16,12 +16,13 @@ * specific language governing permissions and limitations * under the License. */ +var hook = require('require-in-the-middle'); // Ensure, when spawning a new child process, that the `options` and the // `options.env` object passed to the child process function doesn't inherit // from `Object.prototype`. This protects against similar RCE vulnerabilities // as described in CVE-2019-7609 -module.exports = function (cp) { +hook(['child_process'], function (cp) { // The `exec` function is currently just a wrapper around `execFile`. So for // now there's no need to patch it. If this changes in the future, our tests // will fail and we can uncomment the line below. @@ -36,7 +37,7 @@ module.exports = function (cp) { cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptions(true) }); return cp; -}; +}); function patchOptions(hasArgs) { return function apply(target, thisArg, args) { diff --git a/typings/lodash.topath/index.d.ts b/src/setup_node_env/harden/index.js similarity index 87% rename from typings/lodash.topath/index.d.ts rename to src/setup_node_env/harden/index.js index 3630a13f72c284..25cb3bcd78ffb6 100644 --- a/typings/lodash.topath/index.d.ts +++ b/src/setup_node_env/harden/index.js @@ -17,7 +17,5 @@ * under the License. */ -declare module 'lodash/internal/toPath' { - function toPath(value: string | string[]): string[]; - export = toPath; -} +require('./child_process'); +require('./lodash_template'); diff --git a/src/setup_node_env/harden/lodash_template.js b/src/setup_node_env/harden/lodash_template.js new file mode 100644 index 00000000000000..2add624f9f326e --- /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 fbe16e798fff98..182cb8e6e118db 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 12f7eb5a0a0431..6a20261421b5b8 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 d0ff4cc06c57ec..9ea3cf087be909 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/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index e74cd180185ab3..88e6b3a29052e2 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -37,8 +37,17 @@ function flatKeys(source) { export default function ({ getService }) { const supertest = getService('supertest'); + const es = getService('es'); describe('/api/telemetry/v2/clusters/_stats', () => { + before('create some telemetry-data tracked indices', async () => { + return es.indices.create({ index: 'filebeat-telemetry_tests_logs' }); + }); + + after('cleanup telemetry-data tracked indices', () => { + return es.indices.delete({ index: 'filebeat-telemetry_tests_logs' }); + }); + it('should pull local stats and validate data types', async () => { const timeRange = { min: '2018-07-23T22:07:00Z', @@ -71,6 +80,17 @@ export default function ({ getService }) { expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + + // Testing stack_stats.data + expect(stats.stack_stats.data).to.be.an('object'); + expect(stats.stack_stats.data).to.be.an('array'); + expect(stats.stack_stats.data[0]).to.be.an('object'); + expect(stats.stack_stats.data[0].pattern_name).to.be('filebeat'); + expect(stats.stack_stats.data[0].shipper).to.be('filebeat'); + expect(stats.stack_stats.data[0].index_count).to.be(1); + expect(stats.stack_stats.data[0].doc_count).to.be(0); + expect(stats.stack_stats.data[0].ecs_index_count).to.be(0); + expect(stats.stack_stats.data[0].size_in_bytes).to.be.greaterThan(0); }); it('should pull local stats and validate fields', async () => { diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index c69111be6972bb..03db3a2b108f2b 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/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 5c510617fbb017..a492f3858b524f 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -279,5 +279,79 @@ export default function ({ getService, getPageObjects }) { expect(labels).to.eql(expectedLabels); }); }); + + describe('pipeline aggregations', () => { + before(async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + log.debug('clickLineChart'); + await PageObjects.visualize.clickLineChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + describe('parent pipeline', () => { + it('should have an error if bucket is not selected', async () => { + await PageObjects.visEditor.clickMetricEditor(); + log.debug('Metrics agg = Serial diff'); + await PageObjects.visEditor.selectAggregation('Serial diff', 'metrics'); + await testSubjects.existOrFail('bucketsError'); + }); + + it('should apply with selected bucket', async () => { + log.debug('Bucket = X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); + log.debug('Aggregation = Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Serial Diff of Count'); + }); + + it('should change y-axis label to custom', async () => { + log.debug('set custom label of y-axis to "Custom"'); + await PageObjects.visEditor.setCustomLabel('Custom', 1); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Custom'); + }); + + it('should have advanced accordion and json input', async () => { + await testSubjects.click('advancedParams-1'); + await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer'); + }); + }); + + describe('sibling pipeline', () => { + it('should apply with selected bucket', async () => { + log.debug('Metrics agg = Average Bucket'); + await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Overall Average of Count'); + }); + + it('should change sub metric custom label and calculate y-axis title', async () => { + log.debug('set custom label of sub metric to "Cats"'); + await PageObjects.visEditor.setCustomLabel('Cats', '1-metric'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Overall Average of Cats'); + }); + + it('should outer custom label', async () => { + log.debug('set custom label to "Custom"'); + await PageObjects.visEditor.setCustomLabel('Custom', 1); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Custom'); + }); + + it('should have advanced accordion and json input', async () => { + await testSubjects.click('advancedParams-1'); + await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer'); + }); + }); + }); }); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index d6a4fc91481de2..2d35551b04808f 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 00000000000000..170e3a8fba43e5 --- /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 a32782deec65b9..17345d4301882b 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 b67c1c9060a6e2..36bf3409a5421e 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 a270144bd49fea..87e79b295315f0 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 3a71c3aa9d3d6b..e35ef41420dd69 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 e511d7a8fc15ea..66ebe3478fbec6 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 0600ed8e3fbf69..7c495ad605f6d1 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 2bc6f187564472..ec559c3ee479c3 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 4e032001809f26..b5be3cfa99e5df 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 4987e4dbd4e0a6..6c5125ea4e0eb5 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 0a8bc6f1e6f58a..b721cb2fc563a5 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 605676cee363d1..494f2f38e8bffc 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 dd8d971b7df44f..2d81c2bf4e15fb 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 5dff0629222005..aa546e08ea1ba0 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 6b091a5a4503b8..e8e6f82f138828 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 8d859a570ba91f..e1e1568d2f13cb 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 3512ab16a37125..3c66b57bb94162 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 64f846d13c0bfd..fa4a0e40ddee56 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 7491508ee0745a..64d3ad54a23186 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/util/merge_projection/index.ts b/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts index f3ae0752b908eb..9dc1c815bf1692 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/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 7ee8dfa496b577..4e1af6e0dc2392 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 d71d5f2cb480de..3cd04ee032e561 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/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 8a3e2b1a02dac5..26cff5e71b610f 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 f0bc313ab46444..054476af28de1c 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 c1bfce4cdca49e..620ae6708eda0f 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/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index ef7ebe684fadee..3dbb1b2faac020 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -5,7 +5,7 @@ */ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; import React, { useMemo, useCallback, ReactNode } from 'react'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; @@ -58,9 +58,8 @@ function UnoptimizedManagedTable(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 01043f33ec7b7a..b37146f3b3be5b 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 7f99939a0a0d0c..d3a9ade3925a12 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 7aafa9e1fdcec9..de60441f4faa0b 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 9f72ac6d5916e8..447e11eab5e412 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 a26653d3d5294b..99822c0bbc5cad 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 9ce993e8484884..d9781400f22725 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 { diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts index dbb5d6029d0f16..a14d827eeaec5f 100644 --- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts +++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts @@ -58,7 +58,7 @@ describe('Observability dashboard data', () => { transactions: { type: 'number', label: 'Transactions', - value: 6, + value: 2, color: '#6092c0', }, }, @@ -115,5 +115,45 @@ describe('Observability dashboard data', () => { }, }); }); + it('returns transaction stat as 0 when y is undefined', async () => { + callApmApiMock.mockImplementation(() => + Promise.resolve({ + serviceCount: 0, + transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + }) + ); + const response = await fetchLandingPageData( + { + startTime: '1', + endTime: '2', + bucketSize: '3', + }, + { theme } + ); + expect(response).toEqual({ + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: 'Services', + value: 0, + }, + transactions: { + type: 'number', + label: 'Transactions', + value: 0, + color: '#6092c0', + }, + }, + series: { + transactions: { + label: 'Transactions', + coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + color: '#6092c0', + }, + }, + }); + }); }); }); 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 2107565c5facf9..79ccf8dbd6f9ba 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 { sum } from 'lodash'; +import { mean } from 'lodash'; import { Theme } from '@kbn/ui-shared-deps/theme'; import { ApmFetchDataResponse, @@ -48,7 +48,12 @@ export const fetchLandingPageData = async ( 'xpack.apm.observabilityDashboard.stats.transactions', { defaultMessage: 'Transactions' } ), - value: sum(transactionCoordinates.map((coordinates) => coordinates.y)), + value: + mean( + transactionCoordinates + .map(({ y }) => y) + .filter((y) => y && isFinite(y)) + ) || 0, color: theme.euiColorVis1, }, }, diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 408cdd387cbd88..5f23a9329a5832 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -56,7 +56,6 @@ describe('timeseriesFetcher', () => { apmAgentConfigurationIndex: '.apm-agent-configuration', apmCustomLinkIndex: '.apm-custom-link', }, - dynamicIndexPattern: null as any, }, }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts index c9e9db13cecae0..b34d5535d58cc9 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts @@ -11,15 +11,9 @@ import { localUIFilters, localUIFilterNames, } from '../../ui_filters/local_ui_filters/config'; -import { - esKuery, - IIndexPattern, -} from '../../../../../../../src/plugins/data/server'; +import { esKuery } from '../../../../../../../src/plugins/data/server'; -export function getUiFiltersES( - indexPattern: IIndexPattern | undefined, - uiFilters: UIFilters -) { +export function getUiFiltersES(uiFilters: UIFilters) { const { kuery, environment, ...localFilterValues } = uiFilters; const mappedFilters = localUIFilterNames .filter((name) => name in localFilterValues) @@ -35,7 +29,7 @@ export function getUiFiltersES( // remove undefined items from list const esFilters = [ - getKueryUiFilterES(indexPattern, uiFilters.kuery), + getKueryUiFilterES(uiFilters.kuery), getEnvironmentUiFilterES(uiFilters.environment), ] .filter((filter) => !!filter) @@ -44,14 +38,11 @@ export function getUiFiltersES( return esFilters; } -function getKueryUiFilterES( - indexPattern: IIndexPattern | undefined, - kuery?: string -) { - if (!kuery || !indexPattern) { +function getKueryUiFilterES(kuery?: string) { + if (!kuery) { return; } const ast = esKuery.fromKueryExpression(kuery); - return esKuery.toElasticsearchQuery(ast, indexPattern) as ESFilter; + return esKuery.toElasticsearchQuery(ast) as ESFilter; } diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index 892f8f0ddd1051..2d730933e24731 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -19,11 +19,10 @@ import { ESSearchRequest, ESSearchResponse, } from '../../../typings/elasticsearch'; -import { UI_SETTINGS } from '../../../../../../src/plugins/data/server'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; import { pickKeys } from '../../../common/utils/pick_keys'; import { APMRequestHandlerContext } from '../../routes/typings'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; // `type` was deprecated in 7.0 export type APMIndexDocumentParams = Omit, 'type'>; @@ -85,20 +84,19 @@ function addFilterForLegacyData( } // add additional params for search (aka: read) requests -async function getParamsForSearchRequest( - context: APMRequestHandlerContext, - params: ESSearchRequest, - apmOptions?: APMOptions -) { - const { uiSettings } = context.core; - const [indices, includeFrozen] = await Promise.all([ - getApmIndices({ - savedObjectsClient: context.core.savedObjects.client, - config: context.config, - }), - uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), - ]); - +function getParamsForSearchRequest({ + context, + params, + indices, + includeFrozen, + includeLegacyData, +}: { + context: APMRequestHandlerContext; + params: ESSearchRequest; + indices: ApmIndicesConfig; + includeFrozen: boolean; + includeLegacyData?: boolean; +}) { // Get indices for legacy data filter (only those which apply) const apmIndices = Object.values( pickKeys( @@ -112,7 +110,7 @@ async function getParamsForSearchRequest( ) ); return { - ...addFilterForLegacyData(apmIndices, params, apmOptions), // filter out pre-7.0 data + ...addFilterForLegacyData(apmIndices, params, { includeLegacyData }), // filter out pre-7.0 data ignore_throttled: !includeFrozen, // whether to query frozen indices or not }; } @@ -123,6 +121,8 @@ interface APMOptions { interface ClientCreateOptions { clientAsInternalUser?: boolean; + indices: ApmIndicesConfig; + includeFrozen: boolean; } export type ESClient = ReturnType; @@ -134,7 +134,7 @@ function formatObj(obj: Record) { export function getESClient( context: APMRequestHandlerContext, request: KibanaRequest, - { clientAsInternalUser = false }: ClientCreateOptions = {} + { clientAsInternalUser = false, indices, includeFrozen }: ClientCreateOptions ) { const { callAsCurrentUser, @@ -194,11 +194,13 @@ export function getESClient( params: TSearchRequest, apmOptions?: APMOptions ): Promise> => { - const nextParams = await getParamsForSearchRequest( + const nextParams = await getParamsForSearchRequest({ context, params, - apmOptions - ); + indices, + includeFrozen, + ...apmOptions, + }); return callEs('search', nextParams); }, diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 2dd8ed01082fd7..14c9378d991928 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -5,8 +5,8 @@ */ import moment from 'moment'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { KibanaRequest } from '../../../../../../src/core/server'; -import { IIndexPattern } from '../../../../../../src/plugins/data/common'; import { APMConfig } from '../..'; import { getApmIndices, @@ -18,17 +18,13 @@ import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; import { APMRequestHandlerContext } from '../../routes/typings'; import { getESClient } from './es_client'; import { ProcessorEvent } from '../../../common/processor_event'; -import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern'; -function decodeUiFilters( - indexPattern: IIndexPattern | undefined, - uiFiltersEncoded?: string -) { - if (!uiFiltersEncoded || !indexPattern) { +function decodeUiFilters(uiFiltersEncoded?: string) { + if (!uiFiltersEncoded) { return []; } const uiFilters = JSON.parse(uiFiltersEncoded); - return getUiFiltersES(indexPattern, uiFilters); + return getUiFiltersES(uiFilters); } // Explicitly type Setup to prevent TS initialization errors // https://github.com/microsoft/TypeScript/issues/34933 @@ -39,7 +35,6 @@ export interface Setup { ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; - dynamicIndexPattern?: IIndexPattern; } export interface SetupTimeRange { @@ -75,28 +70,33 @@ export async function setupRequest( const { config } = context; const { query } = context.params; - const indices = await getApmIndices({ - savedObjectsClient: context.core.savedObjects.client, - config, - }); + const [indices, includeFrozen] = await Promise.all([ + getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config, + }), + context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), + ]); - const dynamicIndexPattern = await getDynamicIndexPattern({ - context, + const createClientOptions = { indices, - processorEvent: query.processorEvent, - }); + includeFrozen, + }; - const uiFiltersES = decodeUiFilters(dynamicIndexPattern, query.uiFilters); + const uiFiltersES = decodeUiFilters(query.uiFilters); const coreSetupRequest = { indices, - client: getESClient(context, request, { clientAsInternalUser: false }), + client: getESClient(context, request, { + clientAsInternalUser: false, + ...createClientOptions, + }), internalClient: getESClient(context, request, { clientAsInternalUser: true, + ...createClientOptions, }), ml: getMlSetup(context, request), config, - dynamicIndexPattern, }; return { diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts index e78a3c1cec24a5..0d1a4274c16dc9 100644 --- a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts @@ -41,17 +41,18 @@ export async function getTransactionCoordinates({ field: '@timestamp', fixed_interval: bucketSize, min_doc_count: 0, - extended_bounds: { min: start, max: end }, }, }, }, }, }); + const deltaAsMinutes = (end - start) / 1000 / 60; + return ( aggregations?.distribution.buckets.map((bucket) => ({ x: bucket.key, - y: bucket.doc_count, + y: bucket.doc_count / deltaAsMinutes, })) || [] ); } 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 00000000000000..1f4a8a4367fad7 --- /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 00000000000000..08c8aba5f02078 --- /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 00000000000000..a3a7e5c995bfeb --- /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 01cbc1aa9b9895..f6e331a09fa651 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 00000000000000..49d8efebbf43b1 --- /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 835c00b8df239e..2e394f44b25b10 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/transaction_groups/transform.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts index 81dba39e9d7126..b04ff6764675df 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 5af8b9f78cec1e..3c48c14c2a4714 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/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index d1f473b485dc32..fb357040f5781d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -44,7 +44,6 @@ describe('timeseriesFetcher', () => { apmAgentConfigurationIndex: 'myIndex', apmCustomLinkIndex: 'myIndex', }, - dynamicIndexPattern: null as any, }, }); }); diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts index 5fdd6de06089b4..1cecf14f2eeb8d 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -5,7 +5,6 @@ */ import { omit } from 'lodash'; -import { IIndexPattern } from 'src/plugins/data/server'; import { mergeProjection } from '../../../../common/projections/util/merge_projection'; import { Projection } from '../../../../common/projections/typings'; import { UIFilters } from '../../../../typings/ui_filters'; @@ -13,18 +12,16 @@ import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_ import { localUIFilters, LocalUIFilterName } from './config'; export const getLocalFilterQuery = ({ - indexPattern, uiFilters, projection, localUIFilterName, }: { - indexPattern: IIndexPattern | undefined; uiFilters: UIFilters; projection: Projection; localUIFilterName: LocalUIFilterName; }) => { const field = localUIFilters[localUIFilterName]; - const filter = getUiFiltersES(indexPattern, omit(uiFilters, field.name)); + const filter = getUiFiltersES(omit(uiFilters, field.name)); const bucketCountAggregation = projection.body.aggs ? { 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 967314644c246e..588d5c7896db99 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'; @@ -26,7 +26,7 @@ export async function getLocalUIFilters({ uiFilters: UIFilters; localFilterNames: LocalUIFilterName[]; }) { - const { client, dynamicIndexPattern } = setup; + const { client } = setup; const projectionWithoutAggs = cloneDeep(projection); @@ -35,7 +35,6 @@ export async function getLocalUIFilters({ return Promise.all( localFilterNames.map(async (name) => { const query = getLocalFilterQuery({ - indexPattern: dynamicIndexPattern, uiFilters, projection, localUIFilterName: name, @@ -48,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 b21f0ea8d32dbe..92f52dd1552d64 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/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index 018a14ac766892..18bc2986d40615 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -9,6 +9,8 @@ import { createRoute } from './create_route'; import { setupRequest } from '../lib/helpers/setup_request'; import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; +import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern'; +import { getApmIndices } from '../lib/settings/apm_indices/get_apm_indices'; export const staticIndexPatternRoute = createRoute((core) => ({ method: 'POST', @@ -34,8 +36,17 @@ export const dynamicIndexPatternRoute = createRoute(() => ({ ]), }), }, - handler: async ({ context, request }) => { - const { dynamicIndexPattern } = await setupRequest(context, request); + handler: async ({ context }) => { + const indices = await getApmIndices({ + config: context.config, + savedObjectsClient: context.core.savedObjects.client, + }); + + const dynamicIndexPattern = await getDynamicIndexPattern({ + context, + indices, + }); + return { dynamicIndexPattern }; }, })); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 08eba00251e264..74ab717b8de592 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/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index 280645d4de8d05..a47d72751dfc47 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -97,10 +97,7 @@ function createLocalFiltersRoute< query, setup: { ...setup, - uiFiltersES: getUiFiltersES( - setup.dynamicIndexPattern, - omit(parsedUiFilters, filterNames) - ), + uiFiltersES: getUiFiltersES(omit(parsedUiFilters, filterNames)), }, }); 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 e609cd83587ce5..5bf0f51f48355d 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 947e22ee290895..ebac34afa016b9 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 94e4ca46aec190..6bbf269711fbdc 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 8e3f58b18f3918..24a7e5c3af8fa8 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 9e4271c683415d..63a81e0895348a 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 45a5303d8b0db1..3148a6742f76a4 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 0a648e861b386d..5fdc4519f3bd77 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 b568f18924869f..c32c553fffc1bc 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 4839db047c8713..21166454e478ff 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 0b4583f4581aea..4ffd2ff3e0c968 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 6fbaee8736a505..e4b710240de195 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 bae80d3c335104..f79f189f363d4b 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 8d28287b320667..487f17fb89d120 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 a33d000a1f6563..8ae61f7197ee89 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 1449bddf322b53..05ecf467a1d351 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 c098b7772ef111..2bc52fb0eaafc3 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 134efe61c9dcb9..c0ed14965cbd39 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 8c9f8847d8eede..062f782942a824 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 234f5050716699..b9c879a27fd9cf 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 e417821fd4f67d..c69a1fd9b81379 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 28cfac11e76bd9..af4e3af6db6931 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 065b84c490d207..43d5d5f939ce0b 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx @@ -18,7 +18,7 @@ import { Direction, SortDirection, } from '@elastic/eui'; -import { sortByOrder } from 'lodash'; +import { orderBy } from 'lodash'; // @ts-ignore untyped local import { EuiBasicTableColumn } from '@elastic/eui'; import { Paginate, PaginateChildProps } from '../paginate'; @@ -182,7 +182,7 @@ export class WorkpadTemplates extends React.PureComponent< render() { const { templates } = this.props; const { sortField, sortDirection, searchTerm, filterTags } = this.state; - const sortedTemplates = sortByOrder(templates, [sortField, 'name'], [sortDirection, 'asc']); + const sortedTemplates = orderBy(templates, [sortField, 'name'], [sortDirection, 'asc']); const filteredTemplates = sortedTemplates.filter(({ name = '', help = '', tags = [] }) => { const tagMatch = filterTags.length diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx index e0fe6e60c1dab4..f02407ba2897d8 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 48f4a41c7690a5..ecde5d2eb255b9 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 7ca93f440087e2..f713da5419b3e0 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 b4b2354b4cae0f..714a616679bc90 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 17e2712c44b8db..f4e715b1bbc491 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 766e27d95da9b6..770d4403f8587f 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 0f4953ff56d982..83f4984b4a3002 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 78a34a58f5f782..9cd2bdabd3f451 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 3ada8e7b4efdc1..7b39e8b83b0457 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 420b785771bfed..9f71edcc05bf26 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 3d394afaeba501..4b00d061c17ce2 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 021ac41d88d194..9dae2047c30bf8 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -7,6 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { omit } from 'lodash'; import { KibanaResponseFactory, SavedObjectsClientContract } from 'src/core/server'; +import { CanvasWorkpad } from '../../../types'; import { RouteInitializerDeps } from '../'; import { CANVAS_TYPE, @@ -14,7 +15,6 @@ import { API_ROUTE_WORKPAD_STRUCTURES, API_ROUTE_WORKPAD_ASSETS, } from '../../../common/lib/constants'; -import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; @@ -33,8 +33,8 @@ const workpadUpdateHandler = async ( ) => { const now = new Date().toISOString(); - const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); - await savedObjectsClient.create( + const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); + await savedObjectsClient.create( CANVAS_TYPE, { ...workpadObject.attributes, diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 66b0a7bc558cb6..1a5a21985ba72d 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 24273280d9495f..d988de5851cf51 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 546dc6361826a0..20b3292128a2fb 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 6cfb16d1264272..f7f915f1cf0efc 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 36a6bc0a926afb..0339d0883dc46d 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/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 79fd012337b00c..3d85c2e9eb751c 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -10,19 +10,13 @@ const initContext = coreMock.createPluginInitializerContext(); const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); -typeRegistry.getAllTypes.mockReturnValue([ +typeRegistry.getVisibleTypes.mockReturnValue([ { name: 'foo', hidden: false, mappings: { properties: {} }, namespaceType: 'single' as 'single', }, - { - name: 'bar', - hidden: true, - mappings: { properties: {} }, - namespaceType: 'agnostic' as 'agnostic', - }, ]); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 149c1acfb50863..5783b20eae6484 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -80,10 +80,7 @@ export class Plugin { private registerOssFeatures(savedObjects: SavedObjectsServiceStart) { const registry = savedObjects.getTypeRegistry(); - const savedObjectTypes = registry - .getAllTypes() - .filter((t) => !t.hidden) - .map((t) => t.name); + const savedObjectTypes = registry.getVisibleTypes().map((t) => t.name); this.logger.debug( `Registering OSS features with SO types: ${savedObjectTypes.join(', ')}. "includeTimelion": ${ 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 e41035e9365cec..2570d4540b6a6a 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { Feature } from '../common/feature'; -const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue']; +const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const; interface FeatureCapabilities { [featureId: string]: Record; @@ -67,7 +67,7 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UICapabilities { return allFeatureCapabilities.reduce((acc, capabilities) => { - const mergableCapabilities: UICapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); + const mergableCapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); const mergedFeatureCapabilities = { ...mergableCapabilities, diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 648010eeeafeb6..b0b8cf14ff6997 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 e7afc8f12859c4..a1eac5264bb6a8 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/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 05a5ed462d8f72..f9e6234e1415c0 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/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 1931884cf73067..5c249ee474b000 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/index.scss b/x-pack/plugins/index_management/public/index.scss index 0fbf8ea5036c59..02686c4f7d6f34 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/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 00000000000000..99ab129fc36e3f --- /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 d0b4045949d3e1..8d36262b557923 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 0e631b1e333d75..f46a7f3e5a5e4d 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts @@ -14,8 +14,8 @@ export const transformMetricsExplorerData = ( ) => { const { criteria } = params; if (criteria && data) { - const firstSeries = first(data.series); - const series = firstSeries.rows.reduce((acc, row) => { + const firstSeries = first(data.series) as any; + const series = firstSeries.rows.reduce((acc: any, row: any) => { const { timestamp } = row; criteria.forEach((item, index) => { if (!acc[index]) { diff --git a/x-pack/plugins/infra/public/index.scss b/x-pack/plugins/infra/public/index.scss index 05e045c1bd53be..a3d74e3afebe39 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 00000000000000..21946c7c5653ab --- /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 00000000000000..d10ad5dda53204 --- /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 cc4b6967d34fb7..c2b49c43281a9c 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 10320ebbe76098..37203084124f59 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { compose } from 'lodash'; +import { flowRight } from 'lodash'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; @@ -65,7 +65,7 @@ export const RedirectToNodeLogs = ({ const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; - const searchString = compose( + const searchString = flowRight( replaceLogFilterInQueryString(filter), replaceLogPositionInQueryString(getTimeFromLocation(location)), replaceSourceIdInQueryString(sourceId) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 538cd5f7d9525a..3997a7eab44e81 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 a5515895a51a00..b56b409717cc6f 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 0b8773db2dddf0..c2cde7eb15e95f 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 2a218c1c78aa3a..3802366fe2ac5a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -77,7 +77,10 @@ export const MetricsExplorerChart = ({ const dateFormatter = useMemo( () => series.rows.length > 0 - ? niceTimeFormatter([first(series.rows).timestamp, last(series.rows).timestamp]) + ? niceTimeFormatter([ + (first(series.rows) as any).timestamp, + (last(series.rows) as any).timestamp, + ]) : (value: number) => `${value}`, [series.rows] ); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 1b28945320bb62..2dda664a7f675d 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 00000000000000..3de4c40f47cc9b --- /dev/null +++ b/x-pack/plugins/infra/public/test_utils/index.ts @@ -0,0 +1,309 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const FAKE_SNAPSHOT_RESPONSE = { + nodes: [ + { + path: [{ value: 'host-01', label: 'host-01', ip: '192.168.1.10' }], + metrics: [ + { + name: 'memory', + value: 0.002, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'memory', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'cpu', + value: 0.002, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'cpu', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'rx', + value: 4, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'rx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 4 }, + { timestamp: 1593631055000, metric_0: 4 }, + { timestamp: 1593631355000, metric_0: 9 }, + { timestamp: 1593631655000, metric_0: 4 }, + { timestamp: 1593631955000, metric_0: 3 }, + { timestamp: 1593632255000, metric_0: 2 }, + { timestamp: 1593632555000, metric_0: 2 }, + { timestamp: 1593632855000, metric_0: 4 }, + { timestamp: 1593633155000, metric_0: 3 }, + { timestamp: 1593633455000, metric_0: 2 }, + { timestamp: 1593633755000, metric_0: 2 }, + { timestamp: 1593634055000, metric_0: 3 }, + { timestamp: 1593634355000, metric_0: 0 }, + { timestamp: 1593634655000, metric_0: 11 }, + { timestamp: 1593634955000, metric_0: 6 }, + { timestamp: 1593635255000, metric_0: 14 }, + { timestamp: 1593635555000, metric_0: 10 }, + { timestamp: 1593635855000, metric_0: 8 }, + { timestamp: 1593636155000, metric_0: 4 }, + { timestamp: 1593636455000, metric_0: 4 }, + ], + }, + }, + { + name: 'tx', + value: 3, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'tx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 5 }, + { timestamp: 1593631055000, metric_0: 5 }, + { timestamp: 1593631355000, metric_0: 10 }, + { timestamp: 1593631655000, metric_0: 5 }, + { timestamp: 1593631955000, metric_0: 3 }, + { timestamp: 1593632255000, metric_0: 3 }, + { timestamp: 1593632555000, metric_0: 3 }, + { timestamp: 1593632855000, metric_0: 5 }, + { timestamp: 1593633155000, metric_0: 4 }, + { timestamp: 1593633455000, metric_0: 3 }, + { timestamp: 1593633755000, metric_0: 3 }, + { timestamp: 1593634055000, metric_0: 3 }, + { timestamp: 1593634355000, metric_0: 2 }, + { timestamp: 1593634655000, metric_0: 12 }, + { timestamp: 1593634955000, metric_0: 7 }, + { timestamp: 1593635255000, metric_0: 15 }, + { timestamp: 1593635555000, metric_0: 11 }, + { timestamp: 1593635855000, metric_0: 9 }, + { timestamp: 1593636155000, metric_0: 4 }, + { timestamp: 1593636455000, metric_0: 5 }, + ], + }, + }, + ], + }, + { + path: [{ value: 'host-02', label: 'host-02', ip: '192.168.1.11' }], + metrics: [ + { + name: 'memory', + value: 0.001, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'memory', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'cpu', + value: 0.001, + max: 0.00134, + avg: 0.0009833333333333335, + timeseries: { + id: 'cpu', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 0.001 }, + { timestamp: 1593631055000, metric_0: 0.00099 }, + { timestamp: 1593631355000, metric_0: 0.00133 }, + { timestamp: 1593631655000, metric_0: 0.00099 }, + { timestamp: 1593631955000, metric_0: 0.001 }, + { timestamp: 1593632255000, metric_0: 0.00099 }, + { timestamp: 1593632555000, metric_0: 0.00067 }, + { timestamp: 1593632855000, metric_0: 0.001 }, + { timestamp: 1593633155000, metric_0: 0.00099 }, + { timestamp: 1593633455000, metric_0: 0.00099 }, + { timestamp: 1593633755000, metric_0: 0.00099 }, + { timestamp: 1593634055000, metric_0: 0.001 }, + { timestamp: 1593634355000, metric_0: 0.00067 }, + { timestamp: 1593634655000, metric_0: 0.00133 }, + { timestamp: 1593634955000, metric_0: 0.00101 }, + { timestamp: 1593635255000, metric_0: 0.00134 }, + { timestamp: 1593635555000, metric_0: 0.00133 }, + { timestamp: 1593635855000, metric_0: 0.00102 }, + { timestamp: 1593636155000, metric_0: 0.00101 }, + { timestamp: 1593636455000, metric_0: 0.001 }, + ], + }, + }, + { + name: 'rx', + value: 3, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'rx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 3 }, + { timestamp: 1593631055000, metric_0: 3 }, + { timestamp: 1593631355000, metric_0: 8 }, + { timestamp: 1593631655000, metric_0: 3 }, + { timestamp: 1593631955000, metric_0: 2 }, + { timestamp: 1593632255000, metric_0: 1 }, + { timestamp: 1593632555000, metric_0: 1 }, + { timestamp: 1593632855000, metric_0: 3 }, + { timestamp: 1593633155000, metric_0: 2 }, + { timestamp: 1593633455000, metric_0: 1 }, + { timestamp: 1593633755000, metric_0: 1 }, + { timestamp: 1593634055000, metric_0: 2 }, + { timestamp: 1593634355000, metric_0: 0 }, + { timestamp: 1593634655000, metric_0: 10 }, + { timestamp: 1593634955000, metric_0: 5 }, + { timestamp: 1593635255000, metric_0: 13 }, + { timestamp: 1593635555000, metric_0: 9 }, + { timestamp: 1593635855000, metric_0: 7 }, + { timestamp: 1593636155000, metric_0: 2 }, + { timestamp: 1593636455000, metric_0: 3 }, + ], + }, + }, + { + name: 'tx', + value: 3, + max: 13, + avg: 3.761904761904762, + timeseries: { + id: 'tx', + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + rows: [ + { timestamp: 1593630455000, metric_0: 0 }, + { timestamp: 1593630755000, metric_0: 3 }, + { timestamp: 1593631055000, metric_0: 3 }, + { timestamp: 1593631355000, metric_0: 8 }, + { timestamp: 1593631655000, metric_0: 3 }, + { timestamp: 1593631955000, metric_0: 2 }, + { timestamp: 1593632255000, metric_0: 1 }, + { timestamp: 1593632555000, metric_0: 1 }, + { timestamp: 1593632855000, metric_0: 3 }, + { timestamp: 1593633155000, metric_0: 2 }, + { timestamp: 1593633455000, metric_0: 1 }, + { timestamp: 1593633755000, metric_0: 1 }, + { timestamp: 1593634055000, metric_0: 2 }, + { timestamp: 1593634355000, metric_0: 0 }, + { timestamp: 1593634655000, metric_0: 10 }, + { timestamp: 1593634955000, metric_0: 5 }, + { timestamp: 1593635255000, metric_0: 13 }, + { timestamp: 1593635555000, metric_0: 9 }, + { timestamp: 1593635855000, metric_0: 7 }, + { timestamp: 1593636155000, metric_0: 2 }, + { timestamp: 1593636455000, metric_0: 3 }, + ], + }, + }, + ], + }, + ], + interval: '300s', +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index f4dc7de51daee8..e3d23d86c9f56b 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 445911878111f1..1ef86d9e7eac49 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 ba43303bccf0ba..b865454951cd2e 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 5782277e4f4699..4c02593dd00954 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 39db24684e8d35..0ecfa27d0f0a84 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -70,7 +70,7 @@ export const previewMetricThresholdAlert: ( // Get a date histogram using the bucket interval and the lookback interval try { const alertResults = await evaluateAlert(callCluster, params, config, timeframe); - const groups = Object.keys(first(alertResults)); + const groups = Object.keys(first(alertResults) as any); // Now determine how to interpolate this histogram based on the alert interval const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); @@ -90,7 +90,7 @@ export const previewMetricThresholdAlert: ( // buckets would have fired the alert. If the alert interval and bucket interval are the same, // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation // will skip some buckets or read some buckets more than once, depending on the differential - const numberOfResultBuckets = first(alertResults)[group].shouldFire.length; + const numberOfResultBuckets = (first(alertResults) as any)[group].shouldFire.length; const numberOfExecutionBuckets = Math.floor( numberOfResultBuckets / alertResultsPerExecution ); @@ -118,7 +118,8 @@ export const previewMetricThresholdAlert: ( ? await evaluateAlert(callCluster, params, config) : []; const numberOfGroups = - precalculatedNumberOfGroups ?? Math.max(Object.keys(first(currentAlertResults)).length, 1); + precalculatedNumberOfGroups ?? + Math.max(Object.keys(first(currentAlertResults) as any).length, 1); const estimatedTotalBuckets = (lookbackIntervalInSeconds / bucketIntervalInSeconds) * numberOfGroups; // The minimum number of slices is 2. In case we underestimate the total number of buckets @@ -156,7 +157,7 @@ export const previewMetricThresholdAlert: ( return a + b; }) ); - return zippedResult; + return zippedResult as any; } else throw e; } }; diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts index bb1c4c6632af00..317a7da95ce6b7 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts @@ -127,7 +127,7 @@ export const getNodeMetrics = ( avg: null, })); } - const lastBucket = findLastFullBucket(nodeBuckets, options); + const lastBucket = findLastFullBucket(nodeBuckets, options) as any; return options.metrics.map((metric, index) => { const metricResult: SnapshotNodeMetric = { name: metric.type, diff --git a/x-pack/plugins/infra/server/routes/ip_to_hostname.ts b/x-pack/plugins/infra/server/routes/ip_to_hostname.ts index 28b7777c1d688b..08ad266a22f9b3 100644 --- a/x-pack/plugins/infra/server/routes/ip_to_hostname.ts +++ b/x-pack/plugins/infra/server/routes/ip_to_hostname.ts @@ -48,7 +48,7 @@ export const initIpToHostName = ({ framework }: InfraBackendLibs) => { body: { message: 'Host with matching IP address not found.' }, }); } - const hostDoc = first(hits.hits); + const hostDoc = first(hits.hits) as any; return response.ok({ body: { host: hostDoc._source.host.name } }); } catch ({ statusCode = 500, message = 'Unknown error occurred' }) { return response.customError({ diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index 7e3a30e1e69184..b2664d5a6d9fe5 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 559fba0799987d..2b65c42410723d 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/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 5b68cd2beeed43..3ee3039e9e1c49 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 c52471ccfb4f58..0d1f72afa16f1b 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 6ebfd3f28fd9b3..36b7d412bf276f 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 00000000000000..b00809249897bf --- /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 70668c2856f987..849d7bfc63f349 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 19243084f6821a..42d1075e2ee1fd 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 0a9daf0038aab7..4e79bd4fa79970 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 54cb5171f5a3e3..31c6d764464479 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/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 450def54ba1d01..592ca7f7b8380b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx @@ -36,7 +36,10 @@ export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onCl agent.config_id ); - const agentConfigsRequest = useGetAgentConfigs(); + const agentConfigsRequest = useGetAgentConfigs({ + page: 1, + perPage: 1000, + }); const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 98de9ac217af9d..9819a4fa5d7507 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -119,8 +119,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { }, mappings: { properties: { - id: { type: 'keyword' }, - name: { type: 'text' }, + name: { type: 'keyword' }, description: { type: 'text' }, namespace: { type: 'keyword' }, is_default: { type: 'boolean' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index ada35d1825069d..bd00727714c334 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -143,10 +143,12 @@ class AgentConfigService { soClient: SavedObjectsClientContract, options: ListWithKuery ): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, kuery } = options; + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const agentConfigs = await soClient.find({ type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, // To ensure users don't need to know about SO data structure... @@ -273,7 +275,6 @@ class AgentConfigService { soClient, id, { - ...oldAgentConfig, package_configs: uniq( [...((oldAgentConfig.package_configs || []) as string[])].filter( (pkgConfigId) => !packageConfigIds.includes(pkgConfigId) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index c78a9ff8bb7b56..4420135aec9528 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 b6d87c9ca5b2fa..55970607c74abd 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 0efb202eff5325..016a2344cf532f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -61,7 +61,7 @@ async function getEventsCount(soClient: SavedObjectsClientContract, configId?: s perPage: 0, page: 1, sortField: 'timestamp', - sortOrder: 'DESC', + sortOrder: 'desc', defaultSearchOperator: 'AND', }); diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index 3b003f47eb6f92..02e2c8151fac7f 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 635742c82f9a4d..f5fec020bf5b4d 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 06c07da6cd77ae..2de378f7175348 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 c886f4868ad308..5a7546bfee2e07 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -145,10 +145,12 @@ class PackageConfigService { soClient: SavedObjectsClientContract, options: ListWithKuery ): Promise<{ items: PackageConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, kuery } = options; + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const packageConfigs = await soClient.find({ type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, page, perPage, // To ensure users don't need to know about SO data structure... diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts index 2c8134d2e8f92c..dc0f111680490d 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/__jest__/client_integration/fixtures.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts new file mode 100644 index 00000000000000..8dddb2421f03d4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const nestedProcessorsErrorFixture = { + attributes: { + error: { + root_cause: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'csv', + }, + ], + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + ], + }, + ], + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'csv', + }, + ], + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + ], + }, + status: 400, + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts index 8a14ed13f20221..85848b3d2f73cb 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts @@ -42,6 +42,8 @@ export type PipelineFormTestSubjects = | 'submitButton' | 'pageTitle' | 'savePipelineError' + | 'savePipelineError.showErrorsButton' + | 'savePipelineError.hideErrorsButton' | 'pipelineForm' | 'versionToggle' | 'versionField' diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 2cfccbdc6d5783..813057813f1398 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -9,6 +9,8 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { PipelinesCreateTestBed } from './helpers/pipelines_create.helpers'; +import { nestedProcessorsErrorFixture } from './fixtures'; + const { setup } = pageHelpers.pipelinesCreate; jest.mock('@elastic/eui', () => { @@ -163,6 +165,25 @@ describe('', () => { expect(exists('savePipelineError')).toBe(true); expect(find('savePipelineError').text()).toContain(error.message); }); + + test('displays nested pipeline errors as a flat list', async () => { + const { actions, find, exists, waitFor } = testBed; + httpRequestsMockHelpers.setCreatePipelineResponse(undefined, { + body: nestedProcessorsErrorFixture, + }); + + await act(async () => { + actions.clickSubmitButton(); + await waitFor('savePipelineError'); + }); + + expect(exists('savePipelineError')).toBe(true); + expect(exists('savePipelineError.showErrorsButton')).toBe(true); + find('savePipelineError.showErrorsButton').simulate('click'); + expect(exists('savePipelineError.hideErrorsButton')).toBe(true); + expect(exists('savePipelineError.showErrorsButton')).toBe(false); + expect(find('savePipelineError').find('li').length).toBe(8); + }); }); describe('test pipeline', () => { 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 05c9f0a08b0c72..a68e667f4ab432 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 @@ -11,17 +11,18 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline } from '../../../../common/types'; -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 { OnUpdateHandlerArg, OnUpdateHandler, SerializeResult, } 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'; + export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; onCancel: () => void; @@ -116,7 +117,7 @@ export const PipelineForm: React.FunctionComponent = ({ error={form.getErrors()} > {/* Request error */} - {saveError && } + {saveError && } {/* All form fields */} = ({ errorMessage }) => { - return ( - <> - - } - color="danger" - iconType="alert" - data-test-subj="savePipelineError" - > -

{errorMessage}

-
- - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts new file mode 100644 index 00000000000000..1739365eb197df --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { toKnownError } from './error_utils'; +import { nestedProcessorsErrorFixture } from '../../../../../__jest__/client_integration/fixtures'; + +describe('toKnownError', () => { + test('undefined, null, numbers, arrays and bad objects', () => { + expect(toKnownError(undefined)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError(null)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError(123)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError([])).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError({})).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError({ attributes: {} })).toEqual({ + errors: [{ reason: 'An unknown error occurred.' }], + }); + }); + + test('non-processors errors', () => { + expect(toKnownError(new Error('my error'))).toEqual({ errors: [{ reason: 'my error' }] }); + expect(toKnownError({ message: 'my error' })).toEqual({ errors: [{ reason: 'my error' }] }); + }); + + test('processors errors', () => { + expect(toKnownError(nestedProcessorsErrorFixture)).toMatchInlineSnapshot(` + Object { + "errors": Array [ + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "csv", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + ], + } + `); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts new file mode 100644 index 00000000000000..7f32f962f657c3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts @@ -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 * as t from 'io-ts'; +import { flow } from 'fp-ts/lib/function'; +import { isRight } from 'fp-ts/lib/Either'; + +import { i18nTexts } from './i18n_texts'; + +export interface PipelineError { + reason: string; + processorType?: string; +} +interface PipelineErrors { + errors: PipelineError[]; +} + +interface ErrorNode { + reason: string; + processor_type?: string; + suppressed?: ErrorNode[]; +} + +// This is a runtime type (RT) for an error node which is a recursive type +const errorNodeRT = t.recursion('ErrorNode', (ErrorNode) => + t.intersection([ + t.interface({ + reason: t.string, + }), + t.partial({ + processor_type: t.string, + suppressed: t.array(ErrorNode), + }), + ]) +); + +// This is a runtime type for the attributes object we expect to receive from the server +// for processor errors +const errorAttributesObjectRT = t.interface({ + attributes: t.interface({ + error: t.interface({ + root_cause: t.array(errorNodeRT), + }), + }), +}); + +const isProcessorsError = flow(errorAttributesObjectRT.decode, isRight); + +type ErrorAttributesObject = t.TypeOf; + +const flattenErrorsTree = (node: ErrorNode): PipelineError[] => { + const result: PipelineError[] = []; + const recurse = (_node: ErrorNode) => { + result.push({ reason: _node.reason, processorType: _node.processor_type }); + if (_node.suppressed && Array.isArray(_node.suppressed)) { + _node.suppressed.forEach(recurse); + } + }; + recurse(node); + return result; +}; + +export const toKnownError = (error: unknown): PipelineErrors => { + if (typeof error === 'object' && error != null && isProcessorsError(error)) { + const errorAttributes = error as ErrorAttributesObject; + const rootCause = errorAttributes.attributes.error.root_cause[0]; + return { errors: flattenErrorsTree(rootCause) }; + } + + if (typeof error === 'string') { + return { errors: [{ reason: error }] }; + } + + if ( + error instanceof Error || + (typeof error === 'object' && error != null && (error as any).message) + ) { + return { errors: [{ reason: (error as any).message }] }; + } + + return { errors: [{ reason: i18nTexts.errors.unknownError }] }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts new file mode 100644 index 00000000000000..e354541db8e7b2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.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 { i18n } from '@kbn/i18n'; + +export const i18nTexts = { + title: i18n.translate('xpack.ingestPipelines.form.savePipelineError', { + defaultMessage: 'Unable to create pipeline', + }), + errors: { + processor: (processorType: string) => + i18n.translate('xpack.ingestPipelines.form.savePipelineError.processorLabel', { + defaultMessage: '{type} processor', + values: { type: processorType }, + }), + showErrors: (hiddenErrorsCount: number) => + i18n.translate('xpack.ingestPipelines.form.savePipelineError.showAllButton', { + defaultMessage: + 'Show {hiddenErrorsCount, plural, one {# more error} other {# more errors}}', + values: { + hiddenErrorsCount, + }, + }), + hideErrors: (hiddenErrorsCount: number) => + i18n.translate('xpack.ingestPipelines.form.savePip10mbelineError.showFewerButton', { + defaultMessage: 'Hide {hiddenErrorsCount, plural, one {# error} other {# errors}}', + values: { + hiddenErrorsCount, + }, + }), + unknownError: i18n.translate('xpack.ingestPipelines.form.unknownError', { + defaultMessage: 'An unknown error occurred.', + }), + }, +}; diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts similarity index 79% rename from x-pack/plugins/ml/public/application/explorer/select_limit/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts index 5b7040e5c3606a..656691f6394985 100644 --- a/x-pack/plugins/ml/public/application/explorer/select_limit/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/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 { PipelineFormError } from './pipeline_form_error'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx new file mode 100644 index 00000000000000..23fb9a16484347 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; + +import { EuiSpacer, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { useKibana } from '../../../../shared_imports'; + +import { i18nTexts } from './i18n_texts'; +import { toKnownError, PipelineError } from './error_utils'; + +interface Props { + error: unknown; +} + +const numberOfErrorsToDisplay = 5; + +export const PipelineFormError: React.FunctionComponent = ({ error }) => { + const { services } = useKibana(); + const [isShowingAllErrors, setIsShowingAllErrors] = useState(false); + const safeErrorResult = toKnownError(error); + const hasMoreErrors = safeErrorResult.errors.length > numberOfErrorsToDisplay; + const hiddenErrorsCount = safeErrorResult.errors.length - numberOfErrorsToDisplay; + const results = isShowingAllErrors + ? safeErrorResult.errors + : safeErrorResult.errors.slice(0, numberOfErrorsToDisplay); + + const renderErrorListItem = ({ processorType, reason }: PipelineError) => { + return ( + <> + {processorType ? <>{i18nTexts.errors.processor(processorType) + ':'}  : undefined} + {reason} + + ); + }; + + useEffect(() => { + services.notifications.toasts.addDanger({ title: i18nTexts.title }); + }, [services, error]); + return ( + <> + + {results.length > 1 ? ( +
    + {results.map((e, idx) => ( +
  • {renderErrorListItem(e)}
  • + ))} +
+ ) : ( + renderErrorListItem(results[0]) + )} + {hasMoreErrors ? ( + + + {isShowingAllErrors ? ( + setIsShowingAllErrors(false)} + color="danger" + iconSide="right" + iconType="arrowUp" + data-test-subj="hideErrorsButton" + > + {i18nTexts.errors.hideErrors(hiddenErrorsCount)} + + ) : ( + setIsShowingAllErrors(true)} + color="danger" + iconSide="right" + iconType="arrowDown" + data-test-subj="showErrorsButton" + > + {i18nTexts.errors.showErrors(hiddenErrorsCount)} + + )} + + + ) : undefined} +
+ + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index c1ab3852ee7845..c2328bcc9d0ab5 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -10,6 +10,7 @@ import { Pipeline } from '../../../common/types'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; import { pipelineSchema } from './pipeline_schema'; +import { isObjectWithKeys } from './shared'; const bodySchema = schema.object({ name: schema.string(), @@ -70,7 +71,12 @@ export const registerCreateRoute = ({ if (isEsError(error)) { return res.customError({ statusCode: error.statusCode, - body: error, + body: isObjectWithKeys(error.body) + ? { + message: error.message, + attributes: error.body, + } + : error, }); } diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts new file mode 100644 index 00000000000000..1fa794a4fb9961 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/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 { isObjectWithKeys } from './is_object_with_keys'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts new file mode 100644 index 00000000000000..0617bde26cfb61 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 isObjectWithKeys = (value: unknown) => { + return typeof value === 'object' && !!value && Object.keys(value).length > 0; +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 214b293a43c6c8..cd0e3568f0f601 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; import { pipelineSchema } from './pipeline_schema'; +import { isObjectWithKeys } from './shared'; const bodySchema = schema.object(pipelineSchema); @@ -52,7 +53,12 @@ export const registerUpdateRoute = ({ if (isEsError(error)) { return res.customError({ statusCode: error.statusCode, - body: error, + body: isObjectWithKeys(error.body) + ? { + message: error.message, + attributes: error.body, + } + : error, }); } diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 346a5a24c269f4..7da5eaed5155ef 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 d62f3dbcf029a8..b41e93def966e0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -55,7 +55,7 @@ export function getSavedObjectFormat({ state: { datasourceStates, datasourceMetaData: { - filterableIndexPatterns: _.uniq(filterableIndexPatterns, 'id'), + filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'), }, visualization: visualization.getPersistableState(state.visualization.state), query: framePublicAPI.query, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 851a9f4653fec5..94c0f4083dfee9 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 f70df855fe0cb3..0d60bd588f710f 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 87fbf81fceba08..eb7940634d78ea 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 df49ed828a190c..ebb706258cf457 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 34a4384ec0d40e..5b84108b99dd97 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 e8dfbc250c539a..0a3af97f8ad754 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 1a1a34d30f8a8e..815725f4331a64 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 b756cf81a90739..7cc049c107b87c 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 73fd144b9c7f87..45d0ee45fab4c1 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 6a79ce450cd9ad..3bd0685551a4c9 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 a98f63cf9b3606..e9d095bfbcef1d 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 89e2c753f4c76b..111a113a16be76 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 eea00d52a77f95..1ae10e07b0c243 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 101f536993365d..e995c7317b5d81 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 a04f71a9095c56..9e5a0f496357d4 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 3a1aaaa819dc0a..51691ae18a99a8 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 e507bee2a898d5..9473a1523b8ca7 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 fadee01e695d5c..0cd92fd96c952f 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 dd828c6c35300f..401b6d634c6960 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 bbc6a1dc75c3a6..cea84db8b2794f 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 2e29513ba548ba..cfbeb27efb3d0c 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 36e8d9660ab70c..f349cc4dfd6484 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__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap index 48c70e0a4a05b8..8cb30037379da9 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/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 34f2a9111253b7..f433a88e3bdbd0 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 @@ -24,10 +24,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 +327,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -347,7 +350,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 +401,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -434,7 +437,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -471,7 +474,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -509,7 +512,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -554,7 +557,7 @@ describe('xy_expression', () => { args={multiLayerArgs} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -589,7 +592,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -606,7 +609,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 +629,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 +649,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 +674,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -721,7 +724,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -758,7 +761,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 +781,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 +804,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -822,7 +825,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="CEST" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -842,7 +845,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [firstLayer] }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -869,7 +872,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -890,7 +893,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1196,7 +1199,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 +1218,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 +1237,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1252,7 +1255,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 +1277,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1359,7 +1362,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1417,7 +1420,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} @@ -1473,7 +1476,7 @@ describe('xy_expression', () => { args={args} formatFactory={getFormatSpy} timeZone="UTC" - chartTheme={{}} + chartsThemeService={chartsThemeService} histogramBarTarget={50} onClickValue={onClickValue} onSelectRange={onSelectRange} 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 17ed04aa0e9c49..3ff7bd7fda3046 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'; @@ -38,6 +37,7 @@ import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } 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 { getAxesConfiguration } from './axes_configuration'; @@ -59,7 +59,7 @@ export interface XYRender { } type XYChartRenderProps = XYChartProps & { - chartTheme: PartialTheme; + chartsThemeService: ChartsPluginSetup['theme']; formatFactory: FormatFactory; timeZone: string; histogramBarTarget: number; @@ -115,7 +115,7 @@ export const xyChart: ExpressionFunctionDefinition< export const getXyChartRenderer = (dependencies: { formatFactory: Promise; - chartTheme: PartialTheme; + chartsThemeService: ChartsPluginSetup['theme']; histogramBarTarget: number; timeZone: string; }): ExpressionRenderDefinition => ({ @@ -144,7 +144,7 @@ export const getXyChartRenderer = (dependencies: { { return !( @@ -276,6 +278,7 @@ export function XYChart({ legendPosition={legend.position} showLegendExtra={false} theme={chartTheme} + baseTheme={chartBaseTheme} tooltip={{ headerFormatter: (d) => xAxisFormatter.convert(d.value), }} 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 474ea5c5b08cdf..d38f51cb1621aa 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -31,7 +31,7 @@ function getVisualizationType(state: State): VisualizationType | 'mixed' { ); } const visualizationType = visualizationTypes.find((t) => t.id === state.layers[0].seriesType); - const seriesTypes = _.unique(state.layers.map((l) => l.seriesType)); + const seriesTypes = _.uniq(state.layers.map((l) => l.seriesType)); return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; } diff --git a/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js index 3a304e467e0c0d..7d541ec192e0fe 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 54f03605e14d65..6bc57eae41b76d 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 3f2debeebeb46b..8ce04c83afdbf3 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 98c91fca1fcca7..eeb197a58f51da 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/constants.ts b/x-pack/plugins/maps/common/constants.ts index 25f10c7794fdde..98464427cc3482 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -223,6 +223,11 @@ export enum SCALING_TYPES { export const RGBA_0000 = 'rgba(0,0,0,0)'; +export enum MVT_FIELD_TYPE { + STRING = 'String', + NUMBER = 'Number', +} + export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID'; export enum INITIAL_LOCATION { diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts similarity index 100% rename from x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts rename to x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts diff --git a/x-pack/plugins/maps/common/descriptor_types/index.ts b/x-pack/plugins/maps/common/descriptor_types/index.ts index af0f4487f471b0..b0ae065856a5de 100644 --- a/x-pack/plugins/maps/common/descriptor_types/index.ts +++ b/x-pack/plugins/maps/common/descriptor_types/index.ts @@ -5,6 +5,6 @@ */ export * from './data_request_descriptor_types'; -export * from './descriptor_types'; +export * from './sources'; export * from './map_descriptor'; export * from './style_property_descriptor_types'; diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index 00380ca12a4865..027cc886cd7f7f 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -5,6 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { GeoJsonProperties } from 'geojson'; import { Query } from '../../../../../src/plugins/data/common'; import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; @@ -39,8 +40,9 @@ export type Goto = { }; export type TooltipFeature = { - id: number; + id?: number | string; layerId: string; + mbProperties: GeoJsonProperties; }; export type TooltipState = { diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/sources.ts similarity index 86% rename from x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts rename to x-pack/plugins/maps/common/descriptor_types/sources.ts index c7a706ea64f741..e32b5f44c82722 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/sources.ts @@ -7,7 +7,14 @@ import { FeatureCollection } from 'geojson'; import { Query } from 'src/plugins/data/public'; -import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; +import { + AGG_TYPE, + GRID_RESOLUTION, + RENDER_AS, + SORT_ORDER, + SCALING_TYPES, + MVT_FIELD_TYPE, +} from '../constants'; import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; @@ -96,18 +103,34 @@ export type XYZTMSSourceDescriptor = AbstractSourceDescriptor & urlTemplate: string; }; -export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & { +export type MVTFieldDescriptor = { + name: string; + type: MVT_FIELD_TYPE; +}; + +export type TiledSingleLayerVectorSourceSettings = { urlTemplate: string; layerName: string; // These are the min/max zoom levels of the availability of the a particular layerName in the tileset at urlTemplate. // These are _not_ the visible zoom-range of the data on a map. - // Tiled data can be displayed at higher levels of zoom than that they are stored in the tileset. - // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels + // These are important so mapbox does not issue invalid requests based on the zoom level. + + // Tiled layer data cannot be displayed at lower levels of zoom than that they are stored in the tileset. + // e.g. building footprints at level 14 cannot be displayed at level 0. minSourceZoom: number; + // Tiled layer data can be displayed at higher levels of zoom than that they are stored in the tileset. + // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels maxSourceZoom: number; + + fields: MVTFieldDescriptor[]; }; +export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & + TiledSingleLayerVectorSourceSettings & { + tooltipProperties: string[]; + }; + export type GeojsonFileSourceDescriptor = { __featureCollection: FeatureCollection; name: string; @@ -146,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/common/descriptor_types/style_property_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts similarity index 100% rename from x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts rename to x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 51e251a5d8e202..a0d2152e8866c5 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/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index 60d437d2321b52..e0f5c79f1d4278 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -128,6 +128,10 @@ export class ESAggField implements IESAggField { async getCategoricalFieldMetaRequest(size: number): Promise { return this._esDocField ? this._esDocField.getCategoricalFieldMetaRequest(size) : null; } + + supportsAutoDomain(): boolean { + return true; + } } export function esAggFieldsFactory( diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts index dfd5dc05f7b839..410b38e79ffe4e 100644 --- a/x-pack/plugins/maps/public/classes/fields/field.ts +++ b/x-pack/plugins/maps/public/classes/fields/field.ts @@ -20,6 +20,12 @@ export interface IField { isValid(): boolean; getOrdinalFieldMetaRequest(): Promise; getCategoricalFieldMetaRequest(size: number): Promise; + + // Determines whether Maps-app can automatically determine the domain of the field-values + // if this is not the case (e.g. for .mvt tiled data), + // then styling properties that require the domain to be known cannot use this property. + supportsAutoDomain(): boolean; + supportsFieldMeta(): boolean; } @@ -80,4 +86,8 @@ export class AbstractField implements IField { async getCategoricalFieldMetaRequest(size: number): Promise { return null; } + + supportsAutoDomain(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts new file mode 100644 index 00000000000000..eb2bb94b36a690 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AbstractField, IField } from './field'; +import { FIELD_ORIGIN, MVT_FIELD_TYPE } from '../../../common/constants'; +import { ITiledSingleLayerVectorSource, IVectorSource } from '../sources/vector_source'; +import { MVTFieldDescriptor } from '../../../common/descriptor_types'; + +export class MVTField extends AbstractField implements IField { + private readonly _source: ITiledSingleLayerVectorSource; + private readonly _type: MVT_FIELD_TYPE; + constructor({ + fieldName, + type, + source, + origin, + }: { + fieldName: string; + source: ITiledSingleLayerVectorSource; + origin: FIELD_ORIGIN; + type: MVT_FIELD_TYPE; + }) { + super({ fieldName, origin }); + this._source = source; + this._type = type; + } + + getMVTFieldDescriptor(): MVTFieldDescriptor { + return { + type: this._type, + name: this.getName(), + }; + } + + getSource(): IVectorSource { + return this._source; + } + + async getDataType(): Promise { + if (this._type === MVT_FIELD_TYPE.STRING) { + return 'string'; + } else if (this._type === MVT_FIELD_TYPE.NUMBER) { + return 'number'; + } else { + throw new Error(`Unrecognized MVT field-type ${this._type}`); + } + } + + async getLabel(): Promise { + return this.getName(); + } + + supportsAutoDomain() { + return false; + } +} diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts index 6c504daf3e1925..f4625e42ab5deb 100644 --- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts @@ -60,6 +60,10 @@ export class TopTermPercentageField implements IESAggField { return 0; } + supportsAutoDomain(): boolean { + return true; + } + supportsFieldMeta(): boolean { return false; } diff --git a/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts b/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts new file mode 100644 index 00000000000000..8c4eb49d5040d6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { DataRequestContext } from '../../../actions'; +import { DataMeta, MapFilters } from '../../../../common/descriptor_types'; + +export class MockSyncContext implements DataRequestContext { + dataFilters: MapFilters; + isRequestStillActive: (dataId: string, requestToken: symbol) => boolean; + onLoadError: (dataId: string, requestToken: symbol, errorMessage: string) => void; + registerCancelCallback: (requestToken: symbol, callback: () => void) => void; + startLoading: (dataId: string, requestToken: symbol, meta: DataMeta) => void; + stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataMeta) => void; + updateSourceData: (newData: unknown) => void; + + constructor({ dataFilters }: { dataFilters: Partial }) { + const mapFilters: MapFilters = { + filters: [], + timeFilters: { + from: 'now', + to: '15m', + mode: 'relative', + }, + zoom: 0, + ...dataFilters, + }; + + this.dataFilters = mapFilters; + this.isRequestStillActive = sinon.spy(); + this.onLoadError = sinon.spy(); + this.registerCancelCallback = sinon.spy(); + this.startLoading = sinon.spy(); + this.stopLoading = sinon.spy(); + this.updateSourceData = sinon.spy(); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 859d6092dc64d7..368dcda6b3a5ff 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -16,7 +16,7 @@ import { import { getFileUploadComponent } from '../../../kibana_services'; import { GeojsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; -// @ts-ignore +// @ts-expect-error import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js index f6b9bd62802901..adcc86b9d1546e 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js @@ -91,7 +91,7 @@ export class HeatmapLayer extends VectorLayer { resolution: this.getSource().getGridResolution(), }); mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); - mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom()); } getLayerTypeIconName() { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 2250d5663378cd..d6f6ee8fa609ba 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; @@ -325,27 +327,28 @@ export class AbstractLayer implements ILayer { return this._source.getMinZoom(); } + _getMbSourceId() { + return this.getId(); + } + _requiresPrevSourceCleanup(mbMap: unknown) { return false; } _removeStaleMbSourcesAndLayers(mbMap: unknown) { if (this._requiresPrevSourceCleanup(mbMap)) { - // @ts-ignore + // @ts-expect-error const mbStyle = mbMap.getStyle(); - // @ts-ignore + // @ts-expect-error mbStyle.layers.forEach((mbLayer) => { - // @ts-ignore if (this.ownsMbLayerId(mbLayer.id)) { - // @ts-ignore + // @ts-expect-error mbMap.removeLayer(mbLayer.id); } }); - // @ts-ignore Object.keys(mbStyle.sources).some((mbSourceId) => { - // @ts-ignore if (this.ownsMbSourceId(mbSourceId)) { - // @ts-ignore + // @ts-expect-error mbMap.removeSource(mbSourceId); } }); @@ -429,7 +432,7 @@ export class AbstractLayer implements ILayer { throw new Error('Should implement AbstractLayer#ownsMbLayerId'); } - ownsMbSourceId(sourceId: string): boolean { + ownsMbSourceId(mbSourceId: string): boolean { throw new Error('Should implement AbstractLayer#ownsMbSourceId'); } @@ -482,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/tile_layer/tile_layer.js b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js index 02df8acbfffad3..3e2009c24a2e4d 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js @@ -74,8 +74,8 @@ export class TileLayer extends AbstractLayer { return; } - const sourceId = this.getId(); - mbMap.addSource(sourceId, { + const mbSourceId = this._getMbSourceId(); + mbMap.addSource(mbSourceId, { type: 'raster', tiles: [tmsSourceData.url], tileSize: 256, @@ -85,7 +85,7 @@ export class TileLayer extends AbstractLayer { mbMap.addLayer({ id: mbLayerId, type: 'raster', - source: sourceId, + source: mbSourceId, minzoom: this._descriptor.minZoom, maxzoom: this._descriptor.maxZoom, }); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap new file mode 100644 index 00000000000000..f0ae93601ce8a8 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`icon should use vector icon 1`] = ` +
+`; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx new file mode 100644 index 00000000000000..ecd625db344119 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { MockSyncContext } from '../__tests__/mock_sync_context'; +import sinon from 'sinon'; + +jest.mock('../../../kibana_services', () => { + return { + getUiSettings() { + return { + get() { + return false; + }, + }; + }, + }; +}); + +import { shallow } from 'enzyme'; + +import { Feature } from 'geojson'; +import { MVTSingleLayerVectorSource } from '../../sources/mvt_single_layer_vector_source'; +import { + DataRequestDescriptor, + TiledSingleLayerVectorSourceDescriptor, + VectorLayerDescriptor, +} from '../../../../common/descriptor_types'; +import { SOURCE_TYPES } from '../../../../common/constants'; +import { TiledVectorLayer } from './tiled_vector_layer'; + +const defaultConfig = { + urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', + layerName: 'foobar', + minSourceZoom: 4, + maxSourceZoom: 14, +}; + +function createLayer( + layerOptions: Partial = {}, + sourceOptions: Partial = {} +): TiledVectorLayer { + const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + ...defaultConfig, + fields: [], + tooltipProperties: [], + ...sourceOptions, + }; + const mvtSource = new MVTSingleLayerVectorSource(sourceDescriptor); + + const defaultLayerOptions = { + ...layerOptions, + sourceDescriptor, + }; + const layerDescriptor = TiledVectorLayer.createDescriptor(defaultLayerOptions); + return new TiledVectorLayer({ layerDescriptor, source: mvtSource }); +} + +describe('visiblity', () => { + it('should get minzoom from source', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + expect(layer.getMinZoom()).toEqual(4); + }); + it('should get maxzoom from default', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + expect(layer.getMaxZoom()).toEqual(24); + }); + it('should get maxzoom from layer options', async () => { + const layer: TiledVectorLayer = createLayer({ maxZoom: 10 }, {}); + expect(layer.getMaxZoom()).toEqual(10); + }); +}); + +describe('icon', () => { + it('should use vector icon', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + + const iconAndTooltipContent = layer.getCustomIconAndTooltipContent(); + const component = shallow(iconAndTooltipContent.icon); + expect(component).toMatchSnapshot(); + }); +}); + +describe('getFeatureById', () => { + it('should return null feature', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + const feature = layer.getFeatureById('foobar') as Feature; + expect(feature).toEqual(null); + }); +}); + +describe('syncData', () => { + it('Should sync with source-params', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + + const syncContext = new MockSyncContext({ dataFilters: {} }); + + await layer.syncData(syncContext); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.stopLoading); + + // @ts-expect-error + const call = syncContext.stopLoading.getCall(0); + expect(call.args[2]).toEqual(defaultConfig); + }); + + it('Should not resync when no changes to source params', async () => { + const layer1: TiledVectorLayer = createLayer({}, {}); + const syncContext1 = new MockSyncContext({ dataFilters: {} }); + + await layer1.syncData(syncContext1); + + const dataRequestDescriptor: DataRequestDescriptor = { + data: { ...defaultConfig }, + dataId: 'source', + }; + const layer2: TiledVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + {} + ); + const syncContext2 = new MockSyncContext({ dataFilters: {} }); + await layer2.syncData(syncContext2); + // @ts-expect-error + sinon.assert.notCalled(syncContext2.startLoading); + // @ts-expect-error + sinon.assert.notCalled(syncContext2.stopLoading); + }); + + it('Should resync when changes to source params', async () => { + const layer1: TiledVectorLayer = createLayer({}, {}); + const syncContext1 = new MockSyncContext({ dataFilters: {} }); + + await layer1.syncData(syncContext1); + + const dataRequestDescriptor: DataRequestDescriptor = { + data: defaultConfig, + dataId: 'source', + }; + const layer2: TiledVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + { layerName: 'barfoo' } + ); + const syncContext2 = new MockSyncContext({ dataFilters: {} }); + await layer2.syncData(syncContext2); + + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.stopLoading); + + // @ts-expect-error + const call = syncContext2.stopLoading.getCall(0); + expect(call.args[2]).toEqual({ ...defaultConfig, layerName: 'barfoo' }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index a00639aa5fec54..c9ae1c805fa306 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -6,31 +6,30 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; +import { Feature } from 'geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants'; import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { ITiledSingleLayerVectorSource } from '../../sources/vector_source'; import { DataRequestContext } from '../../../actions'; -import { ISource } from '../../sources/source'; import { VectorLayerDescriptor, VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; -import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor'; +import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types'; export class TiledVectorLayer extends VectorLayer { static type = LAYER_TYPE.TILED_VECTOR; static createDescriptor( descriptor: Partial, - mapColors: string[] + mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); layerDescriptor.type = TiledVectorLayer.type; if (!layerDescriptor.style) { - const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors); + const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); } @@ -64,13 +63,16 @@ export class TiledVectorLayer extends VectorLayer { ); const prevDataRequest = this.getSourceDataRequest(); - const canSkip = await canSkipSourceUpdate({ - source: this._source as ISource, - prevDataRequest, - nextMeta: searchFilters, - }); - if (canSkip) { - return null; + if (prevDataRequest) { + const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + const canSkipBecauseNoChanges = + data.layerName === this._source.getLayerName() && + data.minSourceZoom === this._source.getMinZoom() && + data.maxSourceZoom === this._source.getMaxZoom(); + + if (canSkipBecauseNoChanges) { + return null; + } } startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters); @@ -89,37 +91,41 @@ export class TiledVectorLayer extends VectorLayer { } _syncSourceBindingWithMb(mbMap: unknown) { - // @ts-ignore - const mbSource = mbMap.getSource(this.getId()); - if (!mbSource) { - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - // this is possible if the layer was invisible at startup. - // the actions will not perform any data=syncing as an optimization when a layer is invisible - // when turning the layer back into visible, it's possible the url has not been resovled yet. - return; - } + // @ts-expect-error + const mbSource = mbMap.getSource(this._getMbSourceId()); + if (mbSource) { + return; + } + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + // this is possible if the layer was invisible at startup. + // the actions will not perform any data=syncing as an optimization when a layer is invisible + // when turning the layer back into visible, it's possible the url has not been resovled yet. + return; + } - const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if (!sourceMeta) { - return; - } + const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (!sourceMeta) { + return; + } - const sourceId = this.getId(); + const mbSourceId = this._getMbSourceId(); + // @ts-expect-error + mbMap.addSource(mbSourceId, { + type: 'vector', + tiles: [sourceMeta.urlTemplate], + minzoom: sourceMeta.minSourceZoom, + maxzoom: sourceMeta.maxSourceZoom, + }); + } - // @ts-ignore - mbMap.addSource(sourceId, { - type: 'vector', - tiles: [sourceMeta.urlTemplate], - minzoom: sourceMeta.minSourceZoom, - maxzoom: sourceMeta.maxSourceZoom, - }); - } + ownsMbSourceId(mbSourceId: string): boolean { + return this._getMbSourceId() === mbSourceId; } _syncStylePropertiesWithMb(mbMap: unknown) { // @ts-ignore - const mbSource = mbMap.getSource(this.getId()); + const mbSource = mbMap.getSource(this._getMbSourceId()); if (!mbSource) { return; } @@ -129,32 +135,52 @@ export class TiledVectorLayer extends VectorLayer { return; } const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (sourceMeta.layerName === '') { + return; + } this._setMbPointsProperties(mbMap, sourceMeta.layerName); this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); } _requiresPrevSourceCleanup(mbMap: unknown): boolean { - // @ts-ignore - const mbTileSource = mbMap.getSource(this.getId()); + // @ts-expect-error + const mbTileSource = mbMap.getSource(this._getMbSourceId()); if (!mbTileSource) { return false; } + const dataRequest = this.getSourceDataRequest(); if (!dataRequest) { return false; } const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if ( - mbTileSource.tiles[0] === tiledSourceMeta.urlTemplate && - mbTileSource.minzoom === tiledSourceMeta.minSourceZoom && - mbTileSource.maxzoom === tiledSourceMeta.maxSourceZoom - ) { - // TileURL and zoom-range captures all the state. If this does not change, no updates are required. + + if (!tiledSourceMeta) { return false; } - return true; + const isSourceDifferent = + mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate || + mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom || + mbTileSource.maxzoom !== tiledSourceMeta.maxSourceZoom; + + if (isSourceDifferent) { + return true; + } + + const layerIds = this.getMbLayerIds(); + for (let i = 0; i < layerIds.length; i++) { + // @ts-expect-error + const mbLayer = mbMap.getLayer(layerIds[i]); + if (mbLayer && mbLayer.sourceLayer !== tiledSourceMeta.layerName) { + // If the source-pointer of one of the layers is stale, they will all be stale. + // In this case, all the mb-layers need to be removed and re-added. + return true; + } + } + + return false; } syncLayerWithMB(mbMap: unknown) { @@ -171,4 +197,8 @@ export class TiledVectorLayer extends VectorLayer { // higher resolution vector tiles cannot be displayed at lower-res return Math.max(this._source.getMinZoom(), super.getMinZoom()); } + + getFeatureById(id: string | number): Feature | null { + return null; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index e420087628bc83..77daf9c9af5704 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -5,6 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { Feature, GeoJsonProperties } from 'geojson'; import { AbstractLayer } from '../layer'; import { IVectorSource } from '../../sources/vector_source'; import { @@ -17,6 +18,7 @@ import { IJoin } from '../../joins/join'; import { IVectorStyle } from '../../styles/vector/vector_style'; import { IField } from '../../fields/field'; import { DataRequestContext } from '../../../actions'; +import { ITooltipProperty } from '../../tooltips/tooltip_property'; export type VectorLayerArguments = { source: IVectorSource; @@ -31,6 +33,8 @@ export interface IVectorLayer extends ILayer { getValidJoins(): IJoin[]; getSource(): IVectorSource; getStyle(): IVectorStyle; + getFeatureById(id: string | number): Feature | null; + getPropertiesForTooltip(properties: GeoJsonProperties): Promise; } export class VectorLayer extends AbstractLayer implements IVectorLayer { @@ -75,4 +79,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { _setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void; getSource(): IVectorSource; getStyle(): IVectorStyle; + getFeatureById(id: string | number): Feature | null; + getPropertiesForTooltip(properties: GeoJsonProperties): Promise; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 524ab245c67601..0a4fcfc23060c5 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -672,10 +672,10 @@ export class VectorLayer extends AbstractLayer { } this.syncVisibilityWithMb(mbMap, markerLayerId); - mbMap.setLayerZoomRange(markerLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(markerLayerId, this.getMinZoom(), this.getMaxZoom()); if (markerLayerId !== textLayerId) { this.syncVisibilityWithMb(mbMap, textLayerId); - mbMap.setLayerZoomRange(textLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(textLayerId, this.getMinZoom(), this.getMaxZoom()); } } @@ -802,14 +802,14 @@ export class VectorLayer extends AbstractLayer { }); this.syncVisibilityWithMb(mbMap, fillLayerId); - mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom()); const fillFilterExpr = getFillFilterExpression(hasJoins); if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) { mbMap.setFilter(fillLayerId, fillFilterExpr); } this.syncVisibilityWithMb(mbMap, lineLayerId); - mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom()); const lineFilterExpr = getLineFilterExpression(hasJoins); if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { mbMap.setFilter(lineLayerId, lineFilterExpr); @@ -822,9 +822,9 @@ export class VectorLayer extends AbstractLayer { } _syncSourceBindingWithMb(mbMap) { - const mbSource = mbMap.getSource(this.getId()); + const mbSource = mbMap.getSource(this._getMbSourceId()); if (!mbSource) { - mbMap.addSource(this.getId(), { + mbMap.addSource(this._getMbSourceId(), { type: 'geojson', data: EMPTY_FEATURE_COLLECTION, }); @@ -891,16 +891,17 @@ export class VectorLayer extends AbstractLayer { } async getPropertiesForTooltip(properties) { - let allTooltips = await this.getSource().filterAndFormatPropertiesToHtml(properties); - this._addJoinsToSourceTooltips(allTooltips); + const vectorSource = this.getSource(); + let allProperties = await vectorSource.filterAndFormatPropertiesToHtml(properties); + this._addJoinsToSourceTooltips(allProperties); for (let i = 0; i < this.getJoins().length; i++) { const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip( properties ); - allTooltips = [...allTooltips, ...propsFromJoin]; + allProperties = [...allProperties, ...propsFromJoin]; } - return allTooltips; + return allProperties; } canShowTooltip() { @@ -912,7 +913,7 @@ export class VectorLayer extends AbstractLayer { getFeatureById(id) { const featureCollection = this._getSourceFeatureCollection(); if (!featureCollection) { - return; + return null; } return featureCollection.features.find((feature) => { 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 61ec02e72adf22..96dad0c01139e4 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/ems_file_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx index ac69505a9bed5e..7021859ee9827d 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx @@ -15,7 +15,7 @@ import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/vi interface Props { layerId: string; - onChange: (args: OnSourceChangeArgs) => void; + onChange: (...args: OnSourceChangeArgs[]) => void; source: IEmsFileSource; tooltipFields: IField[]; } 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 97afac9ef17457..e20c509ccd4a29 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 b613f577067bac..9431fb55dc88bd 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 076e7a758a4fbd..a4cff7c89a0119 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/__snapshots__/mvt_field_config_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap new file mode 100644 index 00000000000000..f6d0129e85abf6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap @@ -0,0 +1,491 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render error for dupes 1`] = ` + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="String" + /> + + + + + + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="Number" + /> + + + + + + + + + + + Add + + + + +`; + +exports[`should render error for empty name 1`] = ` + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="String" + /> + + + + + + + + + + + Add + + + + +`; + +exports[`should render field editor 1`] = ` + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="String" + /> + + + + + + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="Number" + /> + + + + + + + + + + + Add + + + + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap new file mode 100644 index 00000000000000..699173bd362fae --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap @@ -0,0 +1,211 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render fields-editor when there is no layername 1`] = ` + + + + + + + Available levels + + + + + } + max={24} + min={0} + onChange={[Function]} + prepend="Zoom" + showInput="inputWithPopover" + showLabels={true} + value={ + Array [ + 4, + 14, + ] + } + /> + +`; + +exports[`should render with fields 1`] = ` + + + + + + + Available levels + + + + + } + max={24} + min={0} + onChange={[Function]} + prepend="Zoom" + showInput="inputWithPopover" + showLabels={true} + value={ + Array [ + 4, + 14, + ] + } + /> + + Fields which are available in + + + foobar + + . + + + These can be used for tooltips and dynamic styling. + + } + delay="regular" + position="top" + > + + Fields + + + + + } + labelType="label" + > + + + +`; + +exports[`should render without fields 1`] = ` + + + + + + + Available levels + + + + + } + max={24} + min={0} + onChange={[Function]} + prepend="Zoom" + showInput="inputWithPopover" + showLabels={true} + value={ + Array [ + 4, + 14, + ] + } + /> + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap new file mode 100644 index 00000000000000..ccd0e0064d0754 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render source creation editor (fields should _not_ be included) 1`] = ` + + + + + + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap new file mode 100644 index 00000000000000..bccf2b17e2b5d3 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render update source editor (fields _should_ be included) 1`] = ` + + + +
+ +
+
+ + +
+ + + +
+ +
+
+ + +
+ +
+`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index 067c7f5a47ca35..32fa329be85df5 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -6,23 +6,21 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { - MVTSingleLayerVectorSourceEditor, - MVTSingleLayerVectorSourceConfig, -} from './mvt_single_layer_vector_source_editor'; +import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor'; import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types'; export const mvtVectorSourceWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { - defaultMessage: 'Vector source wizard', + defaultMessage: 'Data service implementing the Mapbox vector tile specification', }), icon: 'grid', renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { + const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx new file mode 100644 index 00000000000000..0121dc45cb9ee6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MVTFieldConfigEditor } from './mvt_field_config_editor'; +import { MVT_FIELD_TYPE } from '../../../../common/constants'; + +test('should render field editor', async () => { + const fields = [ + { + name: 'foo', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'bar', + type: MVT_FIELD_TYPE.NUMBER, + }, + ]; + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); + +test('should render error for empty name', async () => { + const fields = [ + { + name: '', + type: MVT_FIELD_TYPE.STRING, + }, + ]; + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); + +test('should render error for dupes', async () => { + const fields = [ + { + name: 'foo', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'foo', + type: MVT_FIELD_TYPE.NUMBER, + }, + ]; + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx new file mode 100644 index 00000000000000..b2a93a4ef88ad8 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { ChangeEvent, Component, Fragment } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSuperSelect, + EuiFieldText, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; +import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; +import { FieldIcon } from '../../../../../../../src/plugins/kibana_react/public'; +import { MVT_FIELD_TYPE } from '../../../../common/constants'; + +function makeOption({ + value, + icon, + message, +}: { + value: MVT_FIELD_TYPE; + icon: string; + message: string; +}) { + return { + value, + inputDisplay: ( + + + + + {message} + + ), + }; +} + +const FIELD_TYPE_OPTIONS = [ + { + value: MVT_FIELD_TYPE.STRING, + icon: 'string', + message: i18n.translate('xpack.maps.mvtSource.stringFieldLabel', { + defaultMessage: 'string', + }), + }, + { + value: MVT_FIELD_TYPE.NUMBER, + icon: 'number', + message: i18n.translate('xpack.maps.mvtSource.numberFieldLabel', { + defaultMessage: 'number', + }), + }, +].map(makeOption); + +interface Props { + fields: MVTFieldDescriptor[]; + onChange: (fields: MVTFieldDescriptor[]) => void; +} + +interface State { + currentFields: MVTFieldDescriptor[]; +} + +export class MVTFieldConfigEditor extends Component { + state: State = { + currentFields: _.cloneDeep(this.props.fields), + }; + + _notifyChange = _.debounce(() => { + const invalid = this.state.currentFields.some((field: MVTFieldDescriptor) => { + return field.name === ''; + }); + + if (!invalid) { + this.props.onChange(this.state.currentFields); + } + }); + + _fieldChange(newFields: MVTFieldDescriptor[]) { + this.setState( + { + currentFields: newFields, + }, + this._notifyChange + ); + } + + _removeField(index: number) { + const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice(); + newFields.splice(index, 1); + this._fieldChange(newFields); + } + + _addField = () => { + const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice(); + newFields.push({ + type: MVT_FIELD_TYPE.STRING, + name: '', + }); + this._fieldChange(newFields); + }; + + _renderFieldTypeDropDown(mvtFieldConfig: MVTFieldDescriptor, index: number) { + const onChange = (type: MVT_FIELD_TYPE) => { + const newFields = this.state.currentFields.slice(); + newFields[index] = { + type, + name: newFields[index].name, + }; + this._fieldChange(newFields); + }; + + return ( + onChange(value)} + compressed + /> + ); + } + + _renderFieldButtonDelete(index: number) { + return ( + { + this._removeField(index); + }} + title={i18n.translate('xpack.maps.mvtSource.trashButtonTitle', { + defaultMessage: 'Remove field', + })} + aria-label={i18n.translate('xpack.maps.mvtSource.trashButtonAriaLabel', { + defaultMessage: 'Remove field', + })} + /> + ); + } + + _renderFieldNameInput(mvtFieldConfig: MVTFieldDescriptor, index: number) { + const onChange = (e: ChangeEvent) => { + const name = e.target.value; + const newFields = this.state.currentFields.slice(); + newFields[index] = { + name, + type: newFields[index].type, + }; + this._fieldChange(newFields); + }; + + const emptyName = mvtFieldConfig.name === ''; + const hasDupes = + this.state.currentFields.filter((field) => field.name === mvtFieldConfig.name).length > 1; + + return ( + + ); + } + + _renderFieldConfig() { + return this.state.currentFields.map((mvtFieldConfig: MVTFieldDescriptor, index: number) => { + return ( + <> + + {this._renderFieldNameInput(mvtFieldConfig, index)} + {this._renderFieldTypeDropDown(mvtFieldConfig, index)} + {this._renderFieldButtonDelete(index)} + + + + ); + }); + } + + render() { + return ( + + {this._renderFieldConfig()} + + + + + {i18n.translate('xpack.maps.mvtSource.addFieldLabel', { + defaultMessage: 'Add', + })} + + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx new file mode 100644 index 00000000000000..b5c75b97e6cb27 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings'; + +const defaultSettings = { + handleChange: () => {}, + layerName: 'foobar', + fields: [], + minSourceZoom: 4, + maxSourceZoom: 14, + showFields: true, +}; + +test('should render with fields', async () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should render without fields', async () => { + const settings = { ...defaultSettings, showFields: false }; + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should not render fields-editor when there is no layername', async () => { + const settings = { ...defaultSettings, layerName: '' }; + const component = shallow(); + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx new file mode 100644 index 00000000000000..cd3fd97cf66a63 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { Fragment, Component, ChangeEvent } from 'react'; +import { EuiFieldText, EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; +import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public'; +import { MVTFieldConfigEditor } from './mvt_field_config_editor'; +import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; + +export type MVTSettings = { + layerName: string; + fields: MVTFieldDescriptor[]; + minSourceZoom: number; + maxSourceZoom: number; +}; + +interface State { + currentLayerName: string; + currentMinSourceZoom: number; + currentMaxSourceZoom: number; + currentFields: MVTFieldDescriptor[]; +} + +interface Props { + handleChange: (args: MVTSettings) => void; + layerName: string; + fields: MVTFieldDescriptor[]; + minSourceZoom: number; + maxSourceZoom: number; + showFields: boolean; +} + +export class MVTSingleLayerSourceSettings extends Component { + // Tracking in state to allow for debounce. + // Changes to layer-name and/or min/max zoom require heavy operation at map-level (removing and re-adding all sources/layers) + // To preserve snappyness of typing, debounce the dispatches. + state = { + currentLayerName: this.props.layerName, + currentMinSourceZoom: this.props.minSourceZoom, + currentMaxSourceZoom: this.props.maxSourceZoom, + currentFields: _.cloneDeep(this.props.fields), + }; + + _handleChange = _.debounce(() => { + this.props.handleChange({ + layerName: this.state.currentLayerName, + minSourceZoom: this.state.currentMinSourceZoom, + maxSourceZoom: this.state.currentMaxSourceZoom, + fields: this.state.currentFields, + }); + }, 200); + + _handleLayerNameInputChange = (e: ChangeEvent) => { + this.setState({ currentLayerName: e.target.value }, this._handleChange); + }; + + _handleFieldChange = (fields: MVTFieldDescriptor[]) => { + this.setState({ currentFields: fields }, this._handleChange); + }; + + _handleZoomRangeChange = (e: Value) => { + this.setState( + { + currentMinSourceZoom: parseInt(e[0] as string, 10), + currentMaxSourceZoom: parseInt(e[1] as string, 10), + }, + this._handleChange + ); + }; + + render() { + const preMessage = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPreHelpMessage', + { + defaultMessage: 'Fields which are available in ', + } + ); + const message = ( + <> + {this.state.currentLayerName}.{' '} + + ); + const postMessage = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPostHelpMessage', + { + defaultMessage: 'These can be used for tooltips and dynamic styling.', + } + ); + const fieldEditor = + this.props.showFields && this.state.currentLayerName !== '' ? ( + + {preMessage} + {message} + {postMessage} + + } + > + + {i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsMessage', + { + defaultMessage: 'Fields', + } + )}{' '} + + + + } + > + + + ) : null; + + return ( + + + + + + + + {i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.zoomRangeTopMessage', + { + defaultMessage: 'Available levels', + } + )}{' '} + + + + } + formRowDisplay="columnCompressed" + value={[this.state.currentMinSourceZoom, this.state.currentMaxSourceZoom]} + min={MIN_ZOOM} + max={MAX_ZOOM} + onChange={this._handleZoomRangeChange} + allowEmptyRange={false} + showInput="inputWithPopover" + compressed + showLabels + prepend={i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage', + { + defaultMessage: 'Zoom', + } + )} + /> + {fieldEditor} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx new file mode 100644 index 00000000000000..bc08baad7a8429 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.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 { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source'; +import { MVT_FIELD_TYPE, SOURCE_TYPES } from '../../../../common/constants'; +import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types'; + +const descriptor: TiledSingleLayerVectorSourceDescriptor = { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', + layerName: 'foobar', + minSourceZoom: 4, + maxSourceZoom: 14, + fields: [], + tooltipProperties: [], +}; + +describe('getUrlTemplateWithMeta', () => { + it('should echo configuration', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + const config = await source.getUrlTemplateWithMeta(); + expect(config.urlTemplate).toEqual(descriptor.urlTemplate); + expect(config.layerName).toEqual(descriptor.layerName); + expect(config.minSourceZoom).toEqual(descriptor.minSourceZoom); + expect(config.maxSourceZoom).toEqual(descriptor.maxSourceZoom); + }); +}); + +describe('canFormatFeatureProperties', () => { + it('false if no tooltips', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + expect(source.canFormatFeatureProperties()).toEqual(false); + }); + it('true if tooltip', async () => { + const descriptorWithTooltips = { + ...descriptor, + fields: [{ name: 'foobar', type: MVT_FIELD_TYPE.STRING }], + tooltipProperties: ['foobar'], + }; + const source = new MVTSingleLayerVectorSource(descriptorWithTooltips); + expect(source.canFormatFeatureProperties()).toEqual(true); + }); +}); + +describe('filterAndFormatPropertiesToHtml', () => { + const descriptorWithFields = { + ...descriptor, + fields: [ + { + name: 'foo', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'food', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'fooz', + type: MVT_FIELD_TYPE.NUMBER, + }, + ], + tooltipProperties: ['foo', 'fooz'], + }; + + it('should get tooltipproperties', async () => { + const source = new MVTSingleLayerVectorSource(descriptorWithFields); + const tooltipProperties = await source.filterAndFormatPropertiesToHtml({ + foo: 'bar', + fooz: 123, + }); + expect(tooltipProperties.length).toEqual(2); + expect(tooltipProperties[0].getPropertyName()).toEqual('foo'); + expect(tooltipProperties[0].getHtmlDisplayValue()).toEqual('bar'); + expect(tooltipProperties[1].getPropertyName()).toEqual('fooz'); + expect(tooltipProperties[1].getHtmlDisplayValue()).toEqual('123'); + }); +}); + +describe('getImmutableSourceProperties', () => { + it('should only show immutable props', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + const properties = await source.getImmutableProperties(); + expect(properties).toEqual([ + { label: 'Data source', value: '.pbf vector tiles' }, + { label: 'Url', value: 'https://example.com/{x}/{y}/{z}.pbf' }, + ]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts deleted file mode 100644 index 03b91df22d3cab..00000000000000 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ /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 { i18n } from '@kbn/i18n'; -import uuid from 'uuid/v4'; -import { AbstractSource, ImmutableSourceProperty } from '../source'; -import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; -import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; -import { IField } from '../../fields/field'; -import { registerSource } from '../source_registry'; -import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; -import { - MapExtent, - TiledSingleLayerVectorSourceDescriptor, - VectorSourceSyncMeta, -} from '../../../../common/descriptor_types'; -import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor'; -import { ITooltipProperty } from '../../tooltips/tooltip_property'; - -export const sourceTitle = i18n.translate( - 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', - { - defaultMessage: 'Vector Tile Layer', - } -); - -export class MVTSingleLayerVectorSource extends AbstractSource - implements ITiledSingleLayerVectorSource { - static createDescriptor({ - urlTemplate, - layerName, - minSourceZoom, - maxSourceZoom, - }: MVTSingleLayerVectorSourceConfig) { - return { - type: SOURCE_TYPES.MVT_SINGLE_LAYER, - id: uuid(), - urlTemplate, - layerName, - minSourceZoom: Math.max(MIN_ZOOM, minSourceZoom), - maxSourceZoom: Math.min(MAX_ZOOM, maxSourceZoom), - }; - } - - readonly _descriptor: TiledSingleLayerVectorSourceDescriptor; - - constructor( - sourceDescriptor: TiledSingleLayerVectorSourceDescriptor, - inspectorAdapters?: object - ) { - super(sourceDescriptor, inspectorAdapters); - this._descriptor = sourceDescriptor; - } - - renderSourceSettingsEditor() { - return null; - } - - getFieldNames(): string[] { - return []; - } - - getGeoJsonWithMeta( - layerName: 'string', - searchFilters: unknown[], - registerCancelCallback: (callback: () => void) => void - ): Promise { - // todo: remove this method - // This is a consequence of ITiledSingleLayerVectorSource extending IVectorSource. - throw new Error('Does not implement getGeoJsonWithMeta'); - } - - async getFields(): Promise { - return []; - } - - async getImmutableProperties(): Promise { - return [ - { label: getDataSourceLabel(), value: sourceTitle }, - { label: getUrlLabel(), value: this._descriptor.urlTemplate }, - { - label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage', { - defaultMessage: 'Layer name', - }), - value: this._descriptor.layerName, - }, - { - label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage', { - defaultMessage: 'Min zoom', - }), - value: this._descriptor.minSourceZoom.toString(), - }, - { - label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage', { - defaultMessage: 'Max zoom', - }), - value: this._descriptor.maxSourceZoom.toString(), - }, - ]; - } - - async getDisplayName(): Promise { - return this._descriptor.layerName; - } - - async getUrlTemplateWithMeta() { - return { - urlTemplate: this._descriptor.urlTemplate, - layerName: this._descriptor.layerName, - minSourceZoom: this._descriptor.minSourceZoom, - maxSourceZoom: this._descriptor.maxSourceZoom, - }; - } - - async getSupportedShapeTypes(): Promise { - return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; - } - - canFormatFeatureProperties() { - return false; - } - - getMinZoom() { - return this._descriptor.minSourceZoom; - } - - getMaxZoom() { - return this._descriptor.maxSourceZoom; - } - - getBoundsForFilters( - boundsFilters: BoundsFilters, - registerCancelCallback: (requestToken: symbol, callback: () => void) => void - ): MapExtent | null { - return null; - } - - getFieldByName(fieldName: string): IField | null { - return null; - } - - getSyncMeta(): VectorSourceSyncMeta { - return null; - } - - getApplyGlobalQuery(): boolean { - return false; - } - - async filterAndFormatPropertiesToHtml(properties: unknown): Promise { - return []; - } -} - -registerSource({ - ConstructorFunction: MVTSingleLayerVectorSource, - type: SOURCE_TYPES.MVT_SINGLE_LAYER, -}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx new file mode 100644 index 00000000000000..ae28828dec5a82 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import uuid from 'uuid/v4'; +import React from 'react'; +import { GeoJsonProperties } from 'geojson'; +import { AbstractSource, ImmutableSourceProperty, SourceEditorArgs } from '../source'; +import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; +import { + FIELD_ORIGIN, + MAX_ZOOM, + MIN_ZOOM, + SOURCE_TYPES, + VECTOR_SHAPE_TYPE, +} from '../../../../common/constants'; +import { registerSource } from '../source_registry'; +import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; +import { + MapExtent, + MVTFieldDescriptor, + TiledSingleLayerVectorSourceDescriptor, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { MVTField } from '../../fields/mvt_field'; +import { UpdateSourceEditor } from './update_source_editor'; +import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; + +export const sourceTitle = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', + { + defaultMessage: '.pbf vector tiles', + } +); + +export class MVTSingleLayerVectorSource extends AbstractSource + implements ITiledSingleLayerVectorSource { + static createDescriptor({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + fields, + tooltipProperties, + }: Partial) { + return { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + id: uuid(), + urlTemplate: urlTemplate ? urlTemplate : '', + layerName: layerName ? layerName : '', + minSourceZoom: + typeof minSourceZoom === 'number' ? Math.max(MIN_ZOOM, minSourceZoom) : MIN_ZOOM, + maxSourceZoom: + typeof maxSourceZoom === 'number' ? Math.min(MAX_ZOOM, maxSourceZoom) : MAX_ZOOM, + fields: fields ? fields : [], + tooltipProperties: tooltipProperties ? tooltipProperties : [], + }; + } + + readonly _descriptor: TiledSingleLayerVectorSourceDescriptor; + readonly _tooltipFields: MVTField[]; + + constructor( + sourceDescriptor: TiledSingleLayerVectorSourceDescriptor, + inspectorAdapters?: object + ) { + super(sourceDescriptor, inspectorAdapters); + this._descriptor = MVTSingleLayerVectorSource.createDescriptor(sourceDescriptor); + + this._tooltipFields = this._descriptor.tooltipProperties + .map((fieldName) => { + return this.getFieldByName(fieldName); + }) + .filter((f) => f !== null) as MVTField[]; + } + + async supportsFitToBounds() { + return false; + } + + renderSourceSettingsEditor({ onChange }: SourceEditorArgs) { + return ( + + ); + } + + getFieldNames(): string[] { + return this._descriptor.fields.map((field: MVTFieldDescriptor) => { + return field.name; + }); + } + + getMVTFields(): MVTField[] { + return this._descriptor.fields.map((field: MVTFieldDescriptor) => { + return new MVTField({ + fieldName: field.name, + type: field.type, + source: this, + origin: FIELD_ORIGIN.SOURCE, + }); + }); + } + + getFieldByName(fieldName: string): MVTField | null { + try { + return this.createField({ fieldName }); + } catch (e) { + return null; + } + } + + createField({ fieldName }: { fieldName: string }): MVTField { + const field = this._descriptor.fields.find((f: MVTFieldDescriptor) => { + return f.name === fieldName; + }); + if (!field) { + throw new Error(`Cannot create field for fieldName ${fieldName}`); + } + return new MVTField({ + fieldName: field.name, + type: field.type, + source: this, + origin: FIELD_ORIGIN.SOURCE, + }); + } + + getGeoJsonWithMeta( + layerName: 'string', + searchFilters: unknown[], + registerCancelCallback: (callback: () => void) => void + ): Promise { + // Having this method here is a consequence of ITiledSingleLayerVectorSource extending IVectorSource. + throw new Error('Does not implement getGeoJsonWithMeta'); + } + + async getFields(): Promise { + return this.getMVTFields(); + } + + getLayerName(): string { + return this._descriptor.layerName; + } + + async getImmutableProperties(): Promise { + return [ + { label: getDataSourceLabel(), value: sourceTitle }, + { label: getUrlLabel(), value: this._descriptor.urlTemplate }, + ]; + } + + async getDisplayName(): Promise { + return this.getLayerName(); + } + + async getUrlTemplateWithMeta() { + return { + urlTemplate: this._descriptor.urlTemplate, + layerName: this._descriptor.layerName, + minSourceZoom: this._descriptor.minSourceZoom, + maxSourceZoom: this._descriptor.maxSourceZoom, + }; + } + + async getSupportedShapeTypes(): Promise { + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; + } + + canFormatFeatureProperties() { + return !!this._tooltipFields.length; + } + + getMinZoom() { + return this._descriptor.minSourceZoom; + } + + getMaxZoom() { + return this._descriptor.maxSourceZoom; + } + + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null { + return null; + } + + getSyncMeta(): VectorSourceSyncMeta { + return null; + } + + getApplyGlobalQuery(): boolean { + return false; + } + + async filterAndFormatPropertiesToHtml( + properties: GeoJsonProperties, + featureId?: string | number + ): Promise { + const tooltips = []; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + for (let i = 0; i < this._tooltipFields.length; i++) { + const mvtField = this._tooltipFields[i]; + if (mvtField.getName() === key) { + const tooltip = new TooltipProperty(key, key, properties[key]); + tooltips.push(tooltip); + break; + } + } + } + } + return tooltips; + } +} + +registerSource({ + ConstructorFunction: MVTSingleLayerVectorSource, + type: SOURCE_TYPES.MVT_SINGLE_LAYER, +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx new file mode 100644 index 00000000000000..986756f8400145 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor'; + +test('should render source creation editor (fields should _not_ be included)', async () => { + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx index 760b8c676cb37e..49487e96a45440 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx @@ -10,17 +10,14 @@ import _ from 'lodash'; import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; -import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public'; +import { + MVTFieldDescriptor, + TiledSingleLayerVectorSourceSettings, +} from '../../../../common/descriptor_types'; +import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings'; -export type MVTSingleLayerVectorSourceConfig = { - urlTemplate: string; - layerName: string; - minSourceZoom: number; - maxSourceZoom: number; -}; - -export interface Props { - onSourceConfigChange: (sourceConfig: MVTSingleLayerVectorSourceConfig) => void; +interface Props { + onSourceConfigChange: (sourceConfig: TiledSingleLayerVectorSourceSettings) => void; } interface State { @@ -28,6 +25,7 @@ interface State { layerName: string; minSourceZoom: number; maxSourceZoom: number; + fields?: MVTFieldDescriptor[]; } export class MVTSingleLayerVectorSourceEditor extends Component { @@ -36,6 +34,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component { layerName: '', minSourceZoom: MIN_ZOOM, maxSourceZoom: MAX_ZOOM, + fields: [], }; _sourceConfigChange = _.debounce(() => { @@ -50,6 +49,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component { layerName: this.state.layerName, minSourceZoom: this.state.minSourceZoom, maxSourceZoom: this.state.maxSourceZoom, + fields: this.state.fields, }); } }, 200); @@ -64,23 +64,13 @@ export class MVTSingleLayerVectorSourceEditor extends Component { ); }; - _handleLayerNameInputChange = (e: ChangeEvent) => { - const layerName = e.target.value; - this.setState( - { - layerName, - }, - () => this._sourceConfigChange() - ); - }; - - _handleZoomRangeChange = (e: Value) => { - const minSourceZoom = parseInt(e[0] as string, 10); - const maxSourceZoom = parseInt(e[1] as string, 10); - - if (this.state.minSourceZoom !== minSourceZoom || this.state.maxSourceZoom !== maxSourceZoom) { - this.setState({ minSourceZoom, maxSourceZoom }, () => this._sourceConfigChange()); - } + _handleChange = (state: { + layerName: string; + fields: MVTFieldDescriptor[]; + minSourceZoom: number; + maxSourceZoom: number; + }) => { + this.setState(state, () => this._sourceConfigChange()); }; render() { @@ -90,37 +80,30 @@ export class MVTSingleLayerVectorSourceEditor extends Component { label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', { defaultMessage: 'Url', })} - > - - - - + - ); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts new file mode 100644 index 00000000000000..599eaea73c9a01 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; + +export interface MVTSingleLayerVectorSourceConfig { + urlTemplate: string; + layerName: string; + minSourceZoom: number; + maxSourceZoom: number; + fields?: MVTFieldDescriptor[]; + tooltipProperties?: string[]; +} diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx new file mode 100644 index 00000000000000..fd19379058e3b5 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UpdateSourceEditor } from './update_source_editor'; +import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source'; +import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types'; +import { SOURCE_TYPES } from '../../../../common/constants'; + +const descriptor: TiledSingleLayerVectorSourceDescriptor = { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', + layerName: 'foobar', + minSourceZoom: 4, + maxSourceZoom: 14, + fields: [], + tooltipProperties: [], +}; + +test('should render update source editor (fields _should_ be included)', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + + const component = shallow( + {}} /> + ); + + expect(component).toMatchSnapshot(); +}); 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 new file mode 100644 index 00000000000000..8c2f5e271ff5c0 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import React, { Component, Fragment } from 'react'; +import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { TooltipSelector } from '../../../components/tooltip_selector'; +import { MVTField } from '../../fields/mvt_field'; +import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source'; +import { MVTSettings, MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings'; +import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; +import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; + +interface Props { + tooltipFields: MVTField[]; + onChange: (...args: OnSourceChangeArgs[]) => void; + source: MVTSingleLayerVectorSource; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface State {} + +export class UpdateSourceEditor extends Component { + _onTooltipPropertiesSelect = (propertyNames: string[]) => { + this.props.onChange({ propName: 'tooltipProperties', value: propertyNames }); + }; + + _handleChange = (settings: MVTSettings) => { + const changes: OnSourceChangeArgs[] = []; + if (settings.layerName !== this.props.source.getLayerName()) { + changes.push({ propName: 'layerName', value: settings.layerName }); + } + if (settings.minSourceZoom !== this.props.source.getMinZoom()) { + changes.push({ propName: 'minSourceZoom', value: settings.minSourceZoom }); + } + if (settings.maxSourceZoom !== this.props.source.getMaxZoom()) { + changes.push({ propName: 'maxSourceZoom', value: settings.maxSourceZoom }); + } + if (!_.isEqual(settings.fields, this._getFieldDescriptors())) { + changes.push({ propName: 'fields', value: settings.fields }); + + // Remove dangling tooltips. + // This behaves similar to how stale styling properties are removed (e.g. on metric-change in agg sources) + const sanitizedTooltips = []; + for (let i = 0; i < this.props.tooltipFields.length; i++) { + const tooltipName = this.props.tooltipFields[i].getName(); + for (let j = 0; j < settings.fields.length; j++) { + if (settings.fields[j].name === tooltipName) { + sanitizedTooltips.push(tooltipName); + break; + } + } + } + + if (!_.isEqual(sanitizedTooltips, this.props.tooltipFields)) { + changes.push({ propName: 'tooltipProperties', value: sanitizedTooltips }); + } + } + this.props.onChange(...changes); + }; + + _getFieldDescriptors(): MVTFieldDescriptor[] { + return this.props.source.getMVTFields().map((field: MVTField) => { + return field.getMVTFieldDescriptor(); + }); + } + + _renderSourceSettingsCard() { + const fieldDescriptors: MVTFieldDescriptor[] = this._getFieldDescriptors(); + return ( + + + +
+ +
+
+ + +
+ + +
+ ); + } + + _renderTooltipSelectionCard() { + return ( + + + +
+ +
+
+ + + + +
+ + +
+ ); + } + + render() { + return ( + + {this._renderSourceSettingsCard()} + {this._renderTooltipSelectionCard()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index f937eac336532c..c68e22ada8b0c7 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -17,7 +17,7 @@ import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; export type SourceEditorArgs = { - onChange: (args: OnSourceChangeArgs) => void; + onChange: (...args: OnSourceChangeArgs[]) => void; }; export type ImmutableSourceProperty = { diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 99a7478cd83626..42993bf36f6186 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { FeatureCollection } from 'geojson'; +import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; import { Filter, TimeRange } from 'src/plugins/data/public'; import { AbstractSource, ISource } from '../source'; import { IField } from '../../fields/field'; @@ -35,7 +35,7 @@ export type BoundsFilters = { }; export interface IVectorSource extends ISource { - filterAndFormatPropertiesToHtml(properties: unknown): Promise; + filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise; getBoundsForFilters( boundsFilters: BoundsFilters, registerCancelCallback: (requestToken: symbol, callback: () => void) => void @@ -51,10 +51,12 @@ export interface IVectorSource extends ISource { getSyncMeta(): VectorSourceSyncMeta; getFieldNames(): string[]; getApplyGlobalQuery(): boolean; + createField({ fieldName }: { fieldName: string }): IField; + canFormatFeatureProperties(): boolean; } export class AbstractVectorSource extends AbstractSource implements IVectorSource { - filterAndFormatPropertiesToHtml(properties: unknown): Promise; + filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise; getBoundsForFilters( boundsFilters: BoundsFilters, registerCancelCallback: (requestToken: symbol, callback: () => void) => void @@ -72,6 +74,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc canFormatFeatureProperties(): boolean; getApplyGlobalQuery(): boolean; getFieldNames(): string[]; + createField({ fieldName }: { fieldName: string }): IField; } export interface ITiledSingleLayerVectorSource extends IVectorSource { @@ -83,4 +86,5 @@ export interface ITiledSingleLayerVectorSource extends IVectorSource { }>; getMinZoom(): number; getMaxZoom(): number; + getLayerName(): string; } diff --git a/x-pack/plugins/maps/public/classes/styles/style.ts b/x-pack/plugins/maps/public/classes/styles/style.ts index 7d39acd504c426..1859c7875ad11c 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/color/color_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js index b7a80562f10cac..fe2f302504a154 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js @@ -89,7 +89,7 @@ export class ColorMapSelect extends Component { }; _renderColorStopsInput() { - if (!this.props.useCustomColorMap) { + if (!this.props.isCustomOnly && !this.props.useCustomColorMap) { return null; } @@ -102,7 +102,7 @@ export class ColorMapSelect extends Component { swatches={this.props.swatches} /> ); - } else + } else { colorStopEditor = ( ); + } return ( @@ -121,6 +122,10 @@ export class ColorMapSelect extends Component { } _renderColorMapSelections() { + if (this.props.isCustomOnly) { + return null; + } + const colorMapOptionsWithCustom = [ { value: CUSTOM_COLOR_MAP, @@ -146,19 +151,22 @@ export class ColorMapSelect extends Component { ) : null; return ( - - {toggle} - - - - + + + {toggle} + + + + + + ); } @@ -166,7 +174,6 @@ export class ColorMapSelect extends Component { return ( {this._renderColorMapSelections()} - {this._renderColorStopsInput()} ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js index fa13e1cf66664d..90070343a1b48c 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js @@ -90,6 +90,7 @@ export function DynamicColorForm({ if (styleProperty.isOrdinal()) { return ( { + const field = fields.find((field) => { return field.name === selectedFieldName; }); + //Do not spread in all the other unused values (e.g. type, supportsAutoDomain etc...) + if (field) { + selectedOption = { + value: field.value, + label: field.label, + }; + } } return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js index e285d91dcd7a4f..e4dc9d1b4d8f6a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js @@ -46,19 +46,16 @@ export class StyleMapSelect extends Component { }; _renderCustomStopsInput() { - if (!this.props.useCustomMap) { + return !this.props.isCustomOnly && !this.props.useCustomMap + ? null + : this.props.renderCustomStopsInput(this._onCustomMapChange); + } + + _renderMapSelect() { + if (this.props.isCustomOnly) { return null; } - return ( - - - {this.props.renderCustomStopsInput(this._onCustomMapChange)} - - ); - } - - render() { const mapOptionsWithCustom = [ { value: CUSTOM_MAP, @@ -87,6 +84,15 @@ export class StyleMapSelect extends Component { hasDividers={true} compressed /> + + + ); + } + + render() { + return ( + + {this._renderMapSelect()} {this._renderCustomStopsInput()} ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js index f9f8a67846470c..e3724d42a783b7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js @@ -36,17 +36,20 @@ export function DynamicIconForm({ }; function renderIconMapSelect() { - if (!styleOptions.field || !styleOptions.field.name) { + const field = styleProperty.getField(); + if (!field) { return null; } return ( ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js index 08f5dfe4f4ba0f..6cfe656d65a1e3 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js @@ -8,8 +8,8 @@ import React from 'react'; import { StyleMapSelect } from '../style_map_select'; import { i18n } from '@kbn/i18n'; -import { getIconPaletteOptions } from '../../symbol_utils'; import { IconStops } from './icon_stops'; +import { getIconPaletteOptions } from '../../symbol_utils'; export function IconMapSelect({ customIconStops, @@ -19,6 +19,7 @@ export function IconMapSelect({ styleProperty, symbolOptions, useCustomIconMap, + isCustomOnly, }) { function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) { onChange({ @@ -52,6 +53,7 @@ export function IconMapSelect({ useCustomMap={useCustomIconMap} selectedMapId={iconPaletteId} renderCustomStopsInput={renderCustomIconStopsInput} + isCustomOnly={isCustomOnly} /> ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js index 7856a4ddaff395..6528648eff552b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js @@ -62,6 +62,7 @@ export class VectorStyleEditor extends Component { name: field.getName(), origin: field.getOrigin(), type: await field.getDataType(), + supportsAutoDomain: field.supportsAutoDomain(), }; }; @@ -109,7 +110,9 @@ export class VectorStyleEditor extends Component { } _getOrdinalFields() { - return [...this.state.dateFields, ...this.state.numberFields]; + return [...this.state.dateFields, ...this.state.numberFields].filter((field) => { + return field.supportsAutoDomain; + }); } _handleSelectedFeatureChange = (selectedFeature) => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js index ae4d935e2457b7..763eb81ad0f98a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js @@ -10,11 +10,10 @@ import { VECTOR_STYLES } from '../../../../../common/constants'; export class DynamicOrientationProperty extends DynamicStyleProperty { syncIconRotationWithMb(symbolLayerId, mbMap) { - if (this._options.field && this._options.field.name) { - const targetName = getComputedFieldName( - VECTOR_STYLES.ICON_ORIENTATION, - this._options.field.name - ); + if (this._field && this._field.isValid()) { + const targetName = this._field.supportsAutoDomain() + ? getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this.getFieldName()) + : this._field.getName(); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]); } else { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js index de868f3f926506..a7a3130875a955 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js @@ -10,7 +10,11 @@ import { getComputedFieldName } from '../style_util'; export class DynamicTextProperty extends DynamicStyleProperty { syncTextFieldWithMb(mbLayerId, mbMap) { if (this._field && this._field.isValid()) { - const targetName = getComputedFieldName(this._styleName, this._options.field.name); + // Fields that support auto-domain are normalized with a field-formatter and stored into a computed-field + // Otherwise, the raw value is just carried over and no computed field is created. + const targetName = this._field.supportsAutoDomain() + ? getComputedFieldName(this._styleName, this.getFieldName()) + : this._field.getName(); mbMap.setLayoutProperty(mbLayerId, 'text-field', ['coalesce', ['get', targetName], '']); } else { mbMap.setLayoutProperty(mbLayerId, 'text-field', null); 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 04a5381fa25927..3cff48e4d682ed 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 a0dc07b8e545bb..a85cd0cc864075 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/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts index 7149fe29f90ecc..7bb79d8d341d35 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts @@ -19,7 +19,7 @@ export interface ITooltipProperty { export interface LoadFeatureProps { layerId: string; - featureId: number; + featureId?: number | string; } export interface FeatureGeometry { 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 44b7b2ffb6ae71..42c19b8c641e0f 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 00000000000000..0d4f1f99e464ce --- /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 6c5a9af8f0f025..7d4d7bf3ec7ab1 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 00000000000000..bcbeef29875eeb --- /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 1620e3058be678..1c48ed2290dce8 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 0d2732184afc49..00000000000000 --- 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 00000000000000..d2468496fbe0d9 --- /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 bc99285cfc7aa0..00000000000000 --- 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 00000000000000..33d684b320208b --- /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 14252dcfc067d5..71d76ff53d8a93 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 @@ -43,16 +43,16 @@ export class LayerPanel extends React.Component { componentDidMount() { this._isMounted = true; - this.loadDisplayName(); - this.loadImmutableSourceProperties(); - this.loadLeftJoinFields(); + this._loadDisplayName(); + this._loadImmutableSourceProperties(); + this._loadLeftJoinFields(); } componentWillUnmount() { this._isMounted = false; } - loadDisplayName = async () => { + _loadDisplayName = async () => { if (!this.props.selectedLayer) { return; } @@ -63,7 +63,7 @@ export class LayerPanel extends React.Component { } }; - loadImmutableSourceProperties = async () => { + _loadImmutableSourceProperties = async () => { if (!this.props.selectedLayer) { return; } @@ -74,7 +74,7 @@ export class LayerPanel extends React.Component { } }; - async loadLeftJoinFields() { + async _loadLeftJoinFields() { if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) { return; } @@ -97,8 +97,11 @@ export class LayerPanel extends React.Component { } } - _onSourceChange = ({ propName, value, newLayerType }) => { - this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); + _onSourceChange = (...args) => { + for (let i = 0; i < args.length; i++) { + const { propName, value, newLayerType } = args[i]; + this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); + } }; _renderFilterSection() { @@ -202,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/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js index 362186a8f5549b..5e2a153b2ccbfb 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js @@ -31,14 +31,15 @@ export class FeatureProperties extends React.Component { this._isMounted = false; } - _loadProperties = () => { + _loadProperties = async () => { this._fetchProperties({ nextFeatureId: this.props.featureId, nextLayerId: this.props.layerId, + mbProperties: this.props.mbProperties, }); }; - _fetchProperties = async ({ nextLayerId, nextFeatureId }) => { + _fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => { if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) { // do not reload same feature properties return; @@ -64,6 +65,7 @@ export class FeatureProperties extends React.Component { properties = await this.props.loadFeatureProperties({ layerId: nextLayerId, featureId: nextFeatureId, + mbProperties: mbProperties, }); } catch (error) { if (this._isMounted) { diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js index e5b97947602b0d..d91bc8e803ab9e 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js @@ -132,6 +132,7 @@ export class FeaturesTooltip extends React.Component { { 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 00000000000000..273611e94ee40a --- /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 00000000000000..4752eeba2376aa --- /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/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index 7c86d729577e23..84a29db8525391 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -53,7 +53,7 @@ export class TooltipControl extends React.Component { }); } - _getIdsForFeatures(mbFeatures) { + _getTooltipFeatures(mbFeatures) { const uniqueFeatures = []; //there may be duplicates in the results from mapbox //this is because mapbox returns the results per tile @@ -72,9 +72,18 @@ export class TooltipControl extends React.Component { } } if (!match) { + // "tags" (aka properties) are optional in .mvt tiles. + // It's not entirely clear how mapbox-gl handles those. + // - As null value (as defined in https://tools.ietf.org/html/rfc7946#section-3.2) + // - As undefined value + // - As empty object literal + // To avoid ambiguity, normalize properties to empty object literal. + const mbProperties = mbFeature.properties ? mbFeature.properties : {}; + //This keeps track of first properties (assuming these will be identical for features in different tiles uniqueFeatures.push({ id: featureId, layerId: layerId, + mbProperties, }); } } @@ -89,7 +98,7 @@ export class TooltipControl extends React.Component { this._updateHoverTooltipState.cancel(); //ignore any possible moves - const mbFeatures = this._getFeaturesUnderPointer(e.point); + const mbFeatures = this._getMbFeaturesUnderPointer(e.point); if (!mbFeatures.length) { // No features at click location so there is no tooltip to open return; @@ -98,9 +107,9 @@ export class TooltipControl extends React.Component { const targetMbFeataure = mbFeatures[0]; const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure); - const features = this._getIdsForFeatures(mbFeatures); + const features = this._getTooltipFeatures(mbFeatures); this.props.openOnClickTooltip({ - features: features, + features, location: popupAnchorLocation, }); }; @@ -111,7 +120,7 @@ export class TooltipControl extends React.Component { return; } - const mbFeatures = this._getFeaturesUnderPointer(e.point); + const mbFeatures = this._getMbFeaturesUnderPointer(e.point); if (!mbFeatures.length) { this.props.closeOnHoverTooltip(); return; @@ -127,7 +136,7 @@ export class TooltipControl extends React.Component { } const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature); - const features = this._getIdsForFeatures(mbFeatures); + const features = this._getTooltipFeatures(mbFeatures); this.props.openOnHoverTooltip({ features: features, location: popupAnchorLocation, @@ -149,7 +158,7 @@ export class TooltipControl extends React.Component { }); } - _getFeaturesUnderPointer(mbLngLatPoint) { + _getMbFeaturesUnderPointer(mbLngLatPoint) { if (!this.props.mbMap) { return []; } diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js index 31964c3395417b..feac956316f7c5 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js @@ -236,7 +236,7 @@ describe('TooltipControl', () => { sinon.assert.notCalled(closeOnClickTooltipStub); sinon.assert.calledWith(openOnClickTooltipStub, { - features: [{ id: 1, layerId: 'tfi3f' }], + features: [{ id: 1, layerId: 'tfi3f', mbProperties: { __kbn__feature_id__: 1 } }], location: [100, 30], }); }); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js index 03c2aeb2edd0a8..6c420576804084 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -58,7 +58,7 @@ export class TooltipPopover extends Component { // Mapbox feature geometry is from vector tile and is not the same as the original geometry. _loadFeatureGeometry = ({ layerId, featureId }) => { const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { + if (!tooltipLayer || typeof featureId === 'undefined') { return null; } @@ -70,22 +70,24 @@ export class TooltipPopover extends Component { return targetFeature.geometry; }; - _loadFeatureProperties = async ({ layerId, featureId }) => { + _loadFeatureProperties = async ({ layerId, featureId, mbProperties }) => { const tooltipLayer = this._findLayerById(layerId); if (!tooltipLayer) { return []; } - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return []; + let targetFeature; + if (typeof featureId !== 'undefined') { + targetFeature = tooltipLayer.getFeatureById(featureId); } - return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); + + const properties = targetFeature ? targetFeature.properties : mbProperties; + return await tooltipLayer.getPropertiesForTooltip(properties); }; _loadPreIndexedShape = async ({ layerId, featureId }) => { const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { + if (!tooltipLayer || typeof featureId === 'undefined') { return null; } 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 a5934038f83df2..e5801afd5b601a 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 42235bfd5442ee..d96deb226744bb 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 fe974fa610c03e..d2dd07b0f81f91 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 0e29eca2446422..5f57d666b9f748 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 11dc593a235a14..65e914a1ac923a 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 9539d530bab047..9d5125532e5b8f 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 3fb654f35be4de..803281bcd0ce9d 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 27f8c822d68e39..beafae1ecd2f67 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 2a156b5716ad4d..3bc3b8c2c6dfd2 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 07d5a153664b75..95ef5e5b2938c9 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 a423722d1447a1..5715687402bcbf 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 7e5f354bbb4024..63c471e66c49ac 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 590a69283a8194..095b42ffac5b78 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 16e2fb47a209d0..3ad749c9d06314 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 b4d32e2af64b8e..e00e2e1e1e2eb1 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 3ba4ebb2acdeae..d3190d2ac1dade 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 639c0f7b785043..24def01108584c 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 00000000000000..e73aac66a0d9fc --- /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 71c96840d1b579..df4cea0c079874 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 8b30dccc253067..898e29a303881e 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 d1adf8c7ad744c..21e13cb029d69e 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 30ab918983a777..1429bf08583618 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 4e6dcdcc5129ca..aa386288ac7e08 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 2d49fa737cef60..05fdb52e1ccb28 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 bd6a7ee59c942b..23da9669ee9a59 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 a19750494afdc1..068f43a140c901 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 3b92ee3fa37f6a..b85b0401c45cab 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 1614da14e355a4..dd1d0516b6173f 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 a26c0564c6b16d..49f5794273a04d 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 c31b26b7adb7b7..c55c06c80ab81a 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 819f6ca1cac922..be87de7da8c883 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 4e1a2af9b13a60..892b46467345b3 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 cf65419e4bd801..00000000000000 --- 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 7a2df1a0f05350..00000000000000 --- 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 57d1fd81000b7e..e34e1d26c9cab2 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 00000000000000..0607f7fd35fad5 --- /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 2e355c6073abd9..52b4408d1ac5bb 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 ac4882b0055ae0..11ec074bac1db4 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 f0b93c876526ba..c247fd9765e966 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 0944328db00523..f2e362f754f2b9 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 a618534d7ae005..00adb2d3258339 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 7f2bb71d18eb98..d6ccfc2f203e9b 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 af6944d7ae2d24..d1b6f95f32bed5 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 6aa62da3f0768c..d356fc0ef339b9 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 1b2c01ab73fcef..b26528b76037b2 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 9e3fed189b6f48..55ddb1de3529e7 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 86c07a3577f7b6..4f5d0723d65a4f 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 450c166f903004..7411820ba3239e 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 3b4562628051e0..83070a5d94ba09 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 6b2ab89de8a5d0..243369982ac1f8 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 37c2cfb3e029b8..0d587b428d89b6 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 4977ece54bb57f..be9a332e51dbcc 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 54f50d2d3da326..1ffdadb60aaa33 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 63ae89b5acdd1e..846a3f543c2d4d 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 00000000000000..5d91bdb41df6af --- /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 db2b9d55cfabbf..00000000000000 --- 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 890c2bde6305db..a34955adebf62c 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 3829bbce5e5c96..9ed6f88150f68d 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 312b9f31124b13..0db41c1ed104e0 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/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json index 5e185e80a60386..f8feaef3be5f85 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 dc37d05d181110..d312577902f517 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 f6c230a6792fb3..00000000000000 --- 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 00000000000000..77284cb275cd8d --- /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 78cc7901363cda..d58c797b446db6 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/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 726d4be4924d77..9ebb074ec7c3b6 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 b3fc70e9ffd7d1..59e838c449a3b8 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 b76f4eb5b75a79..8232e0a8908d04 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 9c5981585a8de9..9acfce1e8c0b91 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 0ab3683f4b72f1..297ce49f1f148b 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 14f838cff7a3ca..12bd3a7575cf22 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 98d56f9790be47..2255022dcece09 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 9467535d556b0e..163688d7720228 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 5afb382b7cda84..2a4caf17515e11 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 341309004b1106..caa21cd8ee8d79 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 c3fbe266be6d61..cc3682ef764c8b 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 40070a6b0d0f24..19ed8298391d78 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 cf5a99525cc4c3..9508260a64139e 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 06f6cf4f1a5e05..30ec728546ce99 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 ef878e48925570..a5d43d1da7ebca 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 f630903d4e29dc..10a75b9d1ca85e 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 c6575393590f02..74d4bd6d2b5df8 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/indices/get_indices.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js index c087d20a97db1f..ba6d0cb926f063 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js @@ -77,11 +77,11 @@ export function handleResponse(resp, min, max, shardStats) { }); } -export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices'); - - const { min, max } = req.payload.timeRange; - +export function buildGetIndicesQuery( + esIndexPattern, + clusterUuid, + { start, end, size, showSystemIndices = false } +) { const filters = []; if (!showSystemIndices) { filters.push({ @@ -90,14 +90,11 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard }, }); } - - const clusterUuid = req.params.clusterUuid; const metricFields = ElasticsearchMetric.getMetricFields(); - const config = req.server.config(); - const params = { + + return { index: esIndexPattern, - // TODO: composite aggregation - size: config.get('monitoring.ui.max_bucket_size'), + size, ignoreUnavailable: true, filterPath: [ // only filter path can filter for inner_hits @@ -118,8 +115,8 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard body: { query: createQuery({ type: 'index_stats', - start: min, - end: max, + start, + end, clusterUuid, metric: metricFields, filters, @@ -135,9 +132,24 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard sort: [{ timestamp: { order: 'desc' } }], }, }; +} + +export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) { + checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices'); + + const { min: start, max: end } = req.payload.timeRange; + + const clusterUuid = req.params.clusterUuid; + const config = req.server.config(); + const params = buildGetIndicesQuery(esIndexPattern, clusterUuid, { + start, + end, + showSystemIndices, + size: config.get('monitoring.ui.max_bucket_size'), + }); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params).then((resp) => - handleResponse(resp, min, max, shardStats) + handleResponse(resp, start, end, shardStats) ); } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js index 0ac2610bbba629..b07e3511d4804e 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getIndices } from './get_indices'; +export { getIndices, buildGetIndicesQuery } from './get_indices'; export { getIndexSummary } from './get_index_summary'; 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 4e5e439ff90d54..d1a7aec2f153fa 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 87828757755307..39855e7f10eafc 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 2a5c15ece4b40e..e4a36fdf35daf6 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 e65d1779520cf3..e57dfebb36419e 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 d7a708173d3adc..27913fafe32572 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 37441edce0af8d..d0800c7b24fef4 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 898b123e976fd2..bca9496bc9addf 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 5c66bd599dd9ad..3c892fe6120af2 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 db7137c30513bf..305fb6bab54786 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 a4c634439ec45f..acfae5138154cc 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 2a6d08c0740dde..213bea3bc3eec3 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 151eff31f8a010..8c326c3f8a7886 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 d70fbb89c065d4..df9b63bc5fa3de 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 3a61518a850e97..56225639777cdd 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 aa95bbbd9cf0d5..3ebc7e5c8192c4 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 8791b5173b8932..90f53a91e425a5 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 50898f94586fc5..549f6ab0637467 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 a1edf87c33bade..6cf33334d9281d 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 7f58482d35b191..d75c7b585994c7 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 59118ef6f8ec32..3dbbe70bfc5603 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 f21fc2c12a007a..9434747028e531 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 53a3af38f3235b..76be39a2c0e09f 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 c679098bc05bea..35c40e42efc193 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -48,7 +48,7 @@ async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCl const esResponse = await callCluster('search', searchParams); - return get(esResponse, 'hits.hits', []).map((indexPattern) => { + return get(esResponse, 'hits.hits', []).map((indexPattern: any) => { const { _id: savedObjectId } = indexPattern; return getIdFromSavedObjectId(savedObjectId); }); @@ -81,7 +81,7 @@ async function fetchRollupSavedSearches( const savedSearches = get(esResponse, 'hits.hits', []); // Filter for ones with rollup index patterns. - return savedSearches.reduce((rollupSavedSearches, savedSearch) => { + return savedSearches.reduce((rollupSavedSearches: any, savedSearch: any) => { const { _id: savedObjectId, _source: { @@ -136,7 +136,7 @@ async function fetchRollupVisualizations( let rollupVisualizations = 0; let rollupVisualizationsFromSavedSearches = 0; - visualizations.forEach((visualization) => { + visualizations.forEach((visualization: any) => { const { _source: { visualization: { @@ -151,7 +151,7 @@ async function fetchRollupVisualizations( if (savedSearchRefName) { // This visualization depends upon a saved search. - const savedSearch = references.find((ref) => ref.name === savedSearchRefName); + const savedSearch = references.find((ref: any) => ref.name === savedSearchRefName); if (rollupSavedSearchesToFlagMap[savedSearch.id]) { rollupVisualizations++; rollupVisualizationsFromSavedSearches++; diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts index 815fe163411b39..885836780f1a94 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 546d9d628277fe..250947d72c5fab 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 f5daa6b38de48b..e4dded600dcf2e 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 b023f1b365c06a..0fb0522d449bd5 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 10aa59083dff63..14375587c84976 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx @@ -51,7 +51,7 @@ export class ChangeAllPrivilegesControl extends Component { }} disabled={this.props.disabled} > - {_.capitalize(privilege.id)} + {_.upperFirst(privilege.id)} ); }); @@ -65,7 +65,7 @@ export class ChangeAllPrivilegesControl extends Component { }} disabled={this.props.disabled} > - {_.capitalize(NO_PRIVILEGE_VALUE)} + {_.upperFirst(NO_PRIVILEGE_VALUE)} ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index 38e4390a2856ae..a371a9ec9ba1e1 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -15,7 +15,6 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; import React, { Component } from 'react'; import { Role } from '../../../../../../../common/model'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index 7b5d8d8c1ed272..204fb512abcffc 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 585c07c2e834f7..64b7fe3e2e3a90 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 f5c85d3d92be2a..cc9e74805040c3 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 7a977943035582..296a8f6c8693fb 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -12,6 +12,7 @@ import { EuiForm, EuiFormRow, } from '@elastic/eui'; +import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts index 448b7b7e7ef484..8b5c119d594940 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/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index 98f4b4336a1c8e..86cccff9572110 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -60,6 +60,24 @@ export function ancestryArray(event: ResolverEvent): string[] | undefined { return event.process.Ext.ancestry; } +export function getAncestryAsArray(event: ResolverEvent | undefined): string[] { + if (!event) { + return []; + } + + const ancestors = ancestryArray(event); + if (ancestors) { + return ancestors; + } + + const parentID = parentEntityId(event); + if (parentID) { + return [parentID]; + } + + return []; +} + /** * @param event The event to get the category for */ 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 199b8a91e43078..37b73088561960 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/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts index 398e2710b32531..42cbc2327fc288 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -13,7 +13,6 @@ export const validateTree = { params: schema.object({ id: schema.string() }), query: schema.object({ children: schema.number({ defaultValue: 10, min: 0, max: 100 }), - generations: schema.number({ defaultValue: 3, min: 0, max: 3 }), ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }), events: schema.number({ defaultValue: 100, min: 0, max: 1000 }), alerts: schema.number({ defaultValue: 100, min: 0, max: 1000 }), @@ -66,7 +65,6 @@ export const validateChildren = { params: schema.object({ id: schema.string() }), query: schema.object({ children: schema.number({ defaultValue: 10, min: 1, max: 100 }), - generations: schema.number({ defaultValue: 3, min: 1, max: 3 }), afterChild: schema.maybe(schema.string()), legacyEndpointID: schema.maybe(schema.string()), }), diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 4efe89b2429ad6..f2b8acb627cc4c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -77,12 +77,18 @@ export interface ResolverNodeStats { */ export interface ResolverChildNode extends ResolverLifecycleNode { /** - * A child node's pagination cursor can be null for a couple reasons: - * 1. At the time of querying it could have no children in ES, in which case it will be marked as - * null because we know it does not have children during this query. - * 2. If the max level was reached we do not know if this node has children or not so we'll mark it as null + * nextChild can have 3 different states: + * + * undefined: This indicates that you should not use this node for additional queries. It does not mean that node does + * not have any more direct children. The node could have more direct children but to determine that, use the + * ResolverChildren node's nextChild. + * + * null: Indicates that we have received all the children of the node. There may be more descendants though. + * + * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants + * using this node's entity_id */ - nextChild: string | null; + nextChild?: string | null; } /** @@ -92,7 +98,14 @@ export interface ResolverChildNode extends ResolverLifecycleNode { export interface ResolverChildren { childNodes: ResolverChildNode[]; /** - * This is the children cursor for the origin of a tree. + * nextChild can have 2 different states: + * + * null: Indicates that we have received all the descendants that can be retrieved using this node. To retrieve more + * nodes in the tree use a cursor provided in one of the returned children. If no other cursor exists then the tree + * is complete. + * + * string: Indicates this node has more descendants that can be retrieved, pass this cursor in while using this node's + * entity_id for the request. */ nextChild: string | null; } @@ -600,10 +613,8 @@ export interface PolicyConfig { }; malware: MalwareFields; logging: { - stdout: string; file: string; }; - advanced: PolicyConfigAdvancedOptions; }; mac: { events: { @@ -613,10 +624,8 @@ export interface PolicyConfig { }; malware: MalwareFields; logging: { - stdout: string; file: string; }; - advanced: PolicyConfigAdvancedOptions; }; linux: { events: { @@ -625,10 +634,8 @@ export interface PolicyConfig { network: boolean; }; logging: { - stdout: string; file: string; }; - advanced: PolicyConfigAdvancedOptions; }; } @@ -650,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; @@ -673,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_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 9e9732a403f8fc..2a1a2d2c8e1947 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -64,7 +64,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -describe('Detection rules, custom', () => { +// // Skipped as was causing failures on master +describe.skip('Detection rules, custom', () => { before(() => { esArchiverLoad('custom_rule_with_timeline'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index 25fc1fc3a7c110..06e9228de4f490 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,7 +17,8 @@ import { ALERTS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -describe('Export rules', () => { +// Skipped as was causing failures on master +describe.skip('Export rules', () => { before(() => { esArchiverLoad('custom_rules'); cy.server(); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 1ce5243bf79504..703ef6584f1647 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -13,13 +13,11 @@ "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" }, "devDependencies": { - "@types/lodash": "^4.14.110", "@types/md5": "^2.2.0" }, "dependencies": { "@types/rbush": "^3.0.0", "@types/seedrandom": ">=2.0.0 <4.0.0", - "lodash": "^4.17.15", "querystring": "^0.2.0", "rbush": "^3.0.1", "redux-devtools-extension": "^2.13.8" diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts index a3972fd35bf2d5..4b57c7dc20d9f7 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/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 549f0590681bd5..1ed55774f935f2 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/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 b453125223c307..fd75c229d479d2 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/cases/components/filter_popover/index.tsx b/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx index 7b66bcffc89a12..4c16a8c0f3243d 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 6b8e00921abcb7..29f1a2c5a14955 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 > 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 a9ec474a7b6840..6694cec53987b2 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/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 3b3130af77cfd7..9f95284d989a94 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 f079715baec1c1..a3cab1cfabd71b 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 b8ea32969c015b..55e5758775504f 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/utility_bar/utility_bar_action.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx index 250ed75f134c13..f072b27274ed70 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/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 77d4d4364acdd7..23ac6cc5b813d4 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 08c6ec89ff051f..447a70ef998a94 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/map_tool_tip/map_tool_tip.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx index ff6e8859be0494..98d4d3bd8faba7 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { MapToolTipComponent } from './map_tool_tip'; -import { MapFeature } from '../types'; +import { TooltipFeature } from '../../../../../../maps/common/descriptor_types'; describe('MapToolTip', () => { test('placeholder component renders correctly against snapshot', () => { @@ -18,10 +18,11 @@ describe('MapToolTip', () => { test('full component renders correctly against snapshot', () => { const addFilters = jest.fn(); const closeTooltip = jest.fn(); - const features: MapFeature[] = [ + const features: TooltipFeature[] = [ { id: 1, layerId: 'layerId', + mbProperties: {}, }, ]; const getLayerName = jest.fn(); 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 f91fd677ba7fe8..e3ca3c5b842898 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 @@ -36,11 +36,6 @@ export type SetQuery = (params: { refetch: inputsModel.Refetch; }) => void; -export interface MapFeature { - id: number; - layerId: string; -} - export interface FeatureGeometry { coordinates: [number]; type: string; 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 d3bb6123ce04da..ce126bf695559d 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 7296e0ee4b9719..80fe7cb33779a2 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 > ( closePopover={closePopover} panelPaddingSize="none" anchorPosition="downLeft" + repositionOnScroll > 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 ef7ee26cd3ecf5..5af2f3ef488b07 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 83417cdb51b699..0adf7673082695 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 b27f213c6a02c1..d1a58dafcb328f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -204,6 +204,7 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__tdGroupActions ${className}`, }))<{ actionsColumnWidth: number }>` + align-items: center; display: flex; flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`}; min-width: 0; diff --git a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js index 344d0f0e5131a1..5c31b3fad685ab 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/routes/resolver/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts index 74448a324a4ecb..9b8cd9fd3edab1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts @@ -18,14 +18,14 @@ export function handleChildren( return async (context, req, res) => { const { params: { id }, - query: { children, generations, afterChild, legacyEndpointID: endpointID }, + query: { children, afterChild, legacyEndpointID: endpointID }, } = req; try { const client = context.core.elasticsearch.legacy.client; const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); return res.ok({ - body: await fetcher.children(children, generations, afterChild), + body: await fetcher.children(children, afterChild), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts index 95bc612c58a1b6..feb4a404b2359d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts @@ -6,13 +6,13 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; -import { PaginationBuilder, PaginatedResults } from '../utils/pagination'; +import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** * Builds a query for retrieving alerts for a node. */ -export class AlertsQuery extends ResolverQuery { +export class AlertsQuery extends ResolverQuery { constructor( private readonly pagination: PaginationBuilder, indexPattern: string | string[], @@ -38,11 +38,7 @@ export class AlertsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields( - uniquePIDs.length, - 'endgame.serial_event_id', - 'endgame.unique_pid' - ), + ...this.pagination.buildQueryFields('endgame.serial_event_id'), }; } @@ -60,14 +56,11 @@ export class AlertsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'), + ...this.pagination.buildQueryFields('event.id'), }; } - formatResponse(response: SearchResponse): PaginatedResults { - return { - results: ResolverQuery.getResults(response), - totals: PaginationBuilder.getTotals(response.aggregations), - }; + formatResponse(response: SearchResponse): ResolverEvent[] { + return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts index 35f8cad01e6720..1b6a8f2f833874 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts @@ -14,10 +14,12 @@ import { MSearchQuery } from './multi_searcher'; /** * ResolverQuery provides the base structure for queries to retrieve events when building a resolver graph. * - * @param T the structured return type of a resolver query. This represents the type that is returned when translating - * Elasticsearch's SearchResponse response. + * @param T the structured return type of a resolver query. This represents the final return type of the query after handling + * any aggregations. + * @param R the is the type after transforming ES's response. Making this definable let's us set whether it is a resolver event + * or something else. */ -export abstract class ResolverQuery implements MSearchQuery { +export abstract class ResolverQuery implements MSearchQuery { /** * * @param indexPattern the index pattern to use in the query for finding indices with documents in ES. @@ -50,7 +52,7 @@ export abstract class ResolverQuery implements MSearchQuery { }; } - protected static getResults(response: SearchResponse): ResolverEvent[] { + protected getResults(response: SearchResponse): R[] { return response.hits.hits.map((hit) => hit._source); } @@ -68,19 +70,26 @@ export abstract class ResolverQuery implements MSearchQuery { } /** - * Searches ES for the specified ids. + * Searches ES for the specified ids and format the response. * * @param client a client for searching ES * @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid) */ - async search(client: ILegacyScopedClusterClient, ids: string | string[]): Promise { - const res: SearchResponse = await client.callAsCurrentUser( - 'search', - this.buildSearch(ids) - ); + async searchAndFormat(client: ILegacyScopedClusterClient, ids: string | string[]): Promise { + const res: SearchResponse = await this.search(client, ids); return this.formatResponse(res); } + /** + * Searches ES for the specified ids but do not format the response. + * + * @param client a client for searching ES + * @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid) + */ + async search(client: ILegacyScopedClusterClient, ids: string | string[]) { + return client.callAsCurrentUser('search', this.buildSearch(ids)); + } + /** * Builds a query to search the legacy data format. * diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts index a4d4cd546ef608..8175764b3a0a2f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts @@ -25,7 +25,7 @@ describe('Children query', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const msearch: any = query.buildMSearch(['1234', '5678']); expect(msearch[0].index).toBe('index-pattern'); - expect(msearch[1].query.bool.filter[0]).toStrictEqual({ + expect(msearch[1].query.bool.filter[0].bool.should[0]).toStrictEqual({ terms: { 'process.parent.entity_id': ['1234', '5678'] }, }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index b7b1a16926a15d..7fd3808662baa7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -6,13 +6,13 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; -import { PaginationBuilder, PaginatedResults } from '../utils/pagination'; +import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** * Builds a query for retrieving descendants of a node. */ -export class ChildrenQuery extends ResolverQuery { +export class ChildrenQuery extends ResolverQuery { constructor( private readonly pagination: PaginationBuilder, indexPattern: string | string[], @@ -53,11 +53,7 @@ export class ChildrenQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields( - uniquePIDs.length, - 'endgame.serial_event_id', - 'endgame.unique_ppid' - ), + ...this.pagination.buildQueryFields('endgame.serial_event_id'), }; } @@ -67,7 +63,16 @@ export class ChildrenQuery extends ResolverQuery { bool: { filter: [ { - terms: { 'process.parent.entity_id': entityIDs }, + bool: { + should: [ + { + terms: { 'process.parent.entity_id': entityIDs }, + }, + { + terms: { 'process.Ext.ancestry': entityIDs }, + }, + ], + }, }, { term: { 'event.category': 'process' }, @@ -81,14 +86,11 @@ export class ChildrenQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.parent.entity_id'), + ...this.pagination.buildQueryFields('event.id'), }; } - formatResponse(response: SearchResponse): PaginatedResults { - return { - results: ResolverQuery.getResults(response), - totals: PaginationBuilder.getTotals(response.aggregations), - }; + formatResponse(response: SearchResponse): ResolverEvent[] { + return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index ec65e30d1d5d42..abc86826e77dd7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -6,13 +6,13 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; -import { PaginationBuilder, PaginatedResults } from '../utils/pagination'; +import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** * Builds a query for retrieving related events for a node. */ -export class EventsQuery extends ResolverQuery { +export class EventsQuery extends ResolverQuery { constructor( private readonly pagination: PaginationBuilder, indexPattern: string | string[], @@ -45,11 +45,7 @@ export class EventsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields( - uniquePIDs.length, - 'endgame.serial_event_id', - 'endgame.unique_pid' - ), + ...this.pagination.buildQueryFields('endgame.serial_event_id'), }; } @@ -74,14 +70,11 @@ export class EventsQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'), + ...this.pagination.buildQueryFields('event.id'), }; } - formatResponse(response: SearchResponse): PaginatedResults { - return { - results: ResolverQuery.getResults(response), - totals: PaginationBuilder.getTotals(response.aggregations), - }; + formatResponse(response: SearchResponse): ResolverEvent[] { + return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts index 93910293b00aff..0b5728958e91f5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts @@ -60,6 +60,6 @@ export class LifecycleQuery extends ResolverQuery { } formatResponse(response: SearchResponse): ResolverEvent[] { - return ResolverQuery.getResults(response); + return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts index f873ab3019f64c..02dbd92d9252b4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts @@ -5,7 +5,7 @@ */ import { ILegacyScopedClusterClient } from 'kibana/server'; -import { MSearchResponse } from 'elasticsearch'; +import { MSearchResponse, SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; @@ -34,6 +34,10 @@ export interface QueryInfo { * one or many unique identifiers to be searched for in this query */ ids: string | string[]; + /** + * a function to handle the response + */ + handler: (response: SearchResponse) => void; } /** @@ -57,10 +61,10 @@ export class MultiSearcher { throw new Error('No queries provided to MultiSearcher'); } - let searchQuery: JsonObject[] = []; - queries.forEach( - (info) => (searchQuery = [...searchQuery, ...info.query.buildMSearch(info.ids)]) - ); + const searchQuery: JsonObject[] = []; + for (const info of queries) { + searchQuery.push(...info.query.buildMSearch(info.ids)); + } const res: MSearchResponse = await this.client.callAsCurrentUser('msearch', { body: searchQuery, }); @@ -72,6 +76,8 @@ export class MultiSearcher { if (res.responses.length !== queries.length) { throw new Error(`Responses length was: ${res.responses.length} expected ${queries.length}`); } - return res.responses; + for (let i = 0; i < queries.length; i++) { + queries[i].handler(res.responses[i]); + } } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts index a728054bef2197..b8fa409e2ca21a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts @@ -7,13 +7,17 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; -import { AggBucket } from '../utils/pagination'; export interface StatsResult { alerts: Record; events: Record; } +interface AggBucket { + key: string; + doc_count: number; +} + interface CategoriesAgg extends AggBucket { /** * The reason categories is optional here is because if no data was returned in the query the categories aggregation diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts index 181fb8c3df3f92..33011078ee8233 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts @@ -21,7 +21,6 @@ export function handleTree( params: { id }, query: { children, - generations, ancestors, events, alerts, @@ -37,7 +36,7 @@ export function handleTree( const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); const [childrenNodes, ancestry, relatedEvents, relatedAlerts] = await Promise.all([ - fetcher.children(children, generations, afterChild), + fetcher.children(children, afterChild), fetcher.ancestors(ancestors), fetcher.events(events, afterEvent), fetcher.alerts(alerts, afterAlert), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts new file mode 100644 index 00000000000000..ae17cf4c3a562f --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { ResolverRelatedAlerts, ResolverEvent } from '../../../../../common/endpoint/types'; +import { createRelatedAlerts } from './node'; +import { AlertsQuery } from '../queries/alerts'; +import { PaginationBuilder } from './pagination'; +import { QueryInfo } from '../queries/multi_searcher'; +import { SingleQueryHandler } from './fetch'; + +/** + * Requests related alerts for the given node. + */ +export class RelatedAlertsQueryHandler implements SingleQueryHandler { + private relatedAlerts: ResolverRelatedAlerts | undefined; + private readonly query: AlertsQuery; + constructor( + private readonly limit: number, + private readonly entityID: string, + after: string | undefined, + indexPattern: string, + legacyEndpointID: string | undefined + ) { + this.query = new AlertsQuery( + PaginationBuilder.createBuilder(limit, after), + indexPattern, + legacyEndpointID + ); + } + + private handleResponse = (response: SearchResponse) => { + const results = this.query.formatResponse(response); + this.relatedAlerts = createRelatedAlerts( + this.entityID, + results, + PaginationBuilder.buildCursorRequestLimit(this.limit, results) + ); + }; + + /** + * Builds a QueryInfo object that defines the related alerts to search for and how to handle the response. + * + * This will return undefined onces the results have been retrieved from ES. + */ + nextQuery(): QueryInfo | undefined { + if (this.getResults()) { + return; + } + + return { + query: this.query, + ids: this.entityID, + handler: this.handleResponse, + }; + } + + /** + * Get the results after an msearch. + */ + getResults() { + return this.relatedAlerts; + } + + /** + * Perform a regular search and return the results. + * + * @param client the elasticsearch client + */ + async search(client: ILegacyScopedClusterClient) { + const results = this.getResults(); + if (results) { + return results; + } + + this.handleResponse(await this.query.search(client, this.entityID)); + return this.getResults() ?? createRelatedAlerts(this.entityID); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts new file mode 100644 index 00000000000000..9bf16dac791d74 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { + parentEntityId, + entityId, + getAncestryAsArray, +} from '../../../../../common/endpoint/models/event'; +import { + ResolverAncestry, + ResolverEvent, + ResolverLifecycleNode, +} from '../../../../../common/endpoint/types'; +import { createAncestry, createLifecycle } from './node'; +import { LifecycleQuery } from '../queries/lifecycle'; +import { QueryInfo } from '../queries/multi_searcher'; +import { QueryHandler } from './fetch'; + +/** + * Retrieve the ancestry portion of a resolver tree. + */ +export class AncestryQueryHandler implements QueryHandler { + private readonly ancestry: ResolverAncestry = createAncestry(); + private ancestorsToFind: string[]; + private readonly query: LifecycleQuery; + + constructor( + private levels: number, + indexPattern: string, + legacyEndpointID: string | undefined, + originNode: ResolverLifecycleNode | undefined + ) { + this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels); + this.query = new LifecycleQuery(indexPattern, legacyEndpointID); + + // add the origin node to the response if it exists + if (originNode) { + this.ancestry.ancestors.push(originNode); + this.ancestry.nextAncestor = parentEntityId(originNode.lifecycle[0]) || null; + } + } + + private toMapOfNodes(results: ResolverEvent[]) { + return results.reduce((nodes: Map, event: ResolverEvent) => { + const nodeId = entityId(event); + let node = nodes.get(nodeId); + if (!node) { + node = createLifecycle(nodeId, []); + } + + node.lifecycle.push(event); + return nodes.set(nodeId, node); + }, new Map()); + } + + private setNoMore() { + this.ancestry.nextAncestor = null; + this.ancestorsToFind = []; + this.levels = 0; + } + + private handleResponse = (searchResp: SearchResponse) => { + const results = this.query.formatResponse(searchResp); + if (results.length === 0) { + this.setNoMore(); + return; + } + + // bucket the start and end events together for a single node + const ancestryNodes = this.toMapOfNodes(results); + + // the order of this array is going to be weird, it will look like this + // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent] + this.ancestry.ancestors.push(...ancestryNodes.values()); + this.ancestry.nextAncestor = parentEntityId(results[0]) || null; + this.levels = this.levels - ancestryNodes.size; + // the results come back in ascending order on timestamp so the first entry in the + // results should be the further ancestor (most distant grandparent) + this.ancestorsToFind = getAncestryAsArray(results[0]).slice(0, this.levels); + }; + + /** + * Returns whether there are more results to retrieve based on the limit that is passed in and the results that + * have already been received from ES. + */ + hasMore(): boolean { + return this.levels > 0 && this.ancestorsToFind.length > 0; + } + + /** + * Get a query info for retrieving the next set of results. + */ + nextQuery(): QueryInfo | undefined { + if (this.hasMore()) { + return { + query: this.query, + ids: this.ancestorsToFind, + handler: this.handleResponse, + }; + } + } + + /** + * Return the results after using msearch to find them. + */ + getResults() { + return this.ancestry; + } + + /** + * Perform a regular search and return the results. + * + * @param client the elasticsearch client. + */ + async search(client: ILegacyScopedClusterClient) { + while (this.hasMore()) { + const info = this.nextQuery(); + if (!info) { + break; + } + this.handleResponse(await this.query.search(client, info.ids)); + } + return this.getResults(); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 1d55cb7cfd7358..ca5b5aef0f6518 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -3,95 +3,195 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; - -import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { + EndpointDocGenerator, + Tree, + Event, + TreeNode, +} from '../../../../../common/endpoint/generate_data'; import { ChildrenNodesHelper } from './children_helper'; -import { eventId, entityId, parentEntityId } from '../../../../../common/endpoint/models/event'; -import { ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types'; - -function findParents(events: ResolverEvent[]): ResolverEvent[] { - const cache = _.groupBy(events, entityId); +import { eventId, isProcessStart } from '../../../../../common/endpoint/models/event'; - const parents: ResolverEvent[] = []; - Object.values(cache).forEach((lifecycle) => { - const parentNode = cache[parentEntityId(lifecycle[0])!]; - if (parentNode) { - parents.push(parentNode[0]); +function getStartEvents(events: Event[]): Event[] { + const startEvents: Event[] = []; + for (const event of events) { + if (isProcessStart(event)) { + startEvents.push(event); } - }); - return parents; + } + return startEvents; } -function findNode(tree: ResolverChildren, id: string) { - return tree.childNodes.find((node) => { - return node.entityID === id; - }); +function getAllChildrenEvents(tree: Tree) { + const children: Event[] = []; + for (const child of tree.children.values()) { + children.push(...child.lifecycle); + } + return children; +} + +function getStartEventsFromLevels(levels: Array>) { + const startEvents: Event[] = []; + for (const level of levels) { + for (const node of level.values()) { + startEvents.push(...getStartEvents(node.lifecycle)); + } + } + + return startEvents; } describe('Children helper', () => { const generator = new EndpointDocGenerator(); - const root = generator.generateEvent(); + + let tree: Tree; + let helper: ChildrenNodesHelper; + let childrenEvents: Event[]; + let childrenStartEvents: Event[]; + beforeEach(() => { + tree = generator.generateTree({ + children: 3, + alwaysGenMaxChildrenPerNode: true, + generations: 3, + percentTerminated: 100, + ancestryArraySize: 2, + }); + helper = new ChildrenNodesHelper(tree.origin.id, tree.children.size); + childrenEvents = getAllChildrenEvents(tree); + childrenStartEvents = getStartEvents(childrenEvents); + }); + + it('returns the correct entity_ids', () => { + helper.addLifecycleEvents(childrenEvents); + expect(helper.getEntityIDs()).toEqual(Array.from(tree.children.keys())); + }); + + it('returns the correct number of nodes', () => { + helper.addLifecycleEvents(childrenEvents); + expect(helper.getNumNodes()).toEqual(tree.children.size); + }); + + it('marks the query nodes as null', () => { + // +1 indicates that we haven't received all the results so it should create a pagination cursor for the + // queried node (aka the origin that we're passing in) + helper = new ChildrenNodesHelper(tree.origin.id, tree.children.size + 1); + + const nextQuery = helper.addStartEvents(new Set([tree.origin.id]), childrenStartEvents); + helper.addStartEvents(nextQuery!, []); + const nodes = helper.getNodes(); + expect(nodes.nextChild).toBeNull(); + for (const node of nodes.childNodes) { + expect(node.nextChild).toBeNull(); + } + }); + + it('returns undefined when the limit is reached', () => { + helper = new ChildrenNodesHelper(tree.origin.id, tree.children.size - 1); + + expect(helper.addStartEvents(new Set([tree.origin.id]), childrenStartEvents)).toBeUndefined(); + }); + + it('handles multiple additions of start events', () => { + // + 1 indicates that we got everything that ES had + helper = new ChildrenNodesHelper(tree.origin.id, childrenStartEvents.length + 1); + + const level1And2 = getStartEventsFromLevels(tree.childrenLevels.slice(0, 2)); + let nextQuery = helper.addStartEvents(new Set([tree.origin.id]), level1And2); + expect(nextQuery?.size).toEqual(tree.childrenLevels[1].size); + for (const node of tree.childrenLevels[1].values()) { + expect(nextQuery?.has(node.id)).toBeTruthy(); + } + + const level3 = getStartEventsFromLevels(tree.childrenLevels.slice(2, 3)); + nextQuery = helper.addStartEvents(nextQuery!, level3); + expect(nextQuery).toBeUndefined(); + const nodes = helper.getNodes(); + expect(nodes.nextChild).toBeNull(); + for (const node of nodes.childNodes) { + expect(node.nextChild).toBeNull(); + } + }); + + it('handles an empty set', () => { + helper = new ChildrenNodesHelper(tree.origin.id, 1); + + const nextQuery = helper.addStartEvents(new Set([tree.origin.id]), []); + expect(nextQuery).toBeUndefined(); + const nodes = helper.getNodes(); + expect(nodes.nextChild).toBeNull(); + expect(nodes.childNodes.length).toEqual(0); + }); + + it('handles an empty set after multiple additions', () => { + // + 1 indicates that we got everything that ES had + helper = new ChildrenNodesHelper(tree.origin.id, childrenStartEvents.length + 1); + + const level1And2 = getStartEventsFromLevels(tree.childrenLevels.slice(0, 2)); + let nextQuery = helper.addStartEvents(new Set([tree.origin.id]), level1And2); + + nextQuery = helper.addStartEvents(nextQuery!, []); + expect(nextQuery).toBeUndefined(); + const nodes = helper.getNodes(); + expect(nodes.nextChild).toBeNull(); + for (const node of nodes.childNodes) { + expect(node.nextChild).toBeNull(); + } + }); + + it('non leaf nodes are set to undefined by default', () => { + // + 1 indicates that we got everything that ES had + helper = new ChildrenNodesHelper(tree.origin.id, childrenStartEvents.length + 1); + const level1And2 = getStartEventsFromLevels(tree.childrenLevels.slice(0, 2)); + helper.addStartEvents(new Set([tree.origin.id]), level1And2); + const nodes = helper.getNodes(); + expect(nodes.nextChild).toBeNull(); + for (const node of nodes.childNodes) { + if (tree.childrenLevels[0].has(node.entityID)) { + expect(node.nextChild).toBeNull(); + } else { + expect(node.nextChild).toBeUndefined(); + } + } + }); + + it('returns the leaf nodes', () => { + helper = new ChildrenNodesHelper(tree.origin.id, tree.children.size + 1); + + const nextQuery = helper.addStartEvents(new Set([tree.origin.id]), childrenStartEvents); + // we're using an ancestry array of 2 so the leaf nodes are at the second level + expect(nextQuery?.size).toEqual(tree.childrenLevels[1].size); + + for (const node of tree.childrenLevels[1].values()) { + expect(nextQuery?.has(node.id)).toBeTruthy(); + } + }); it('builds the children response structure', () => { - const children = Array.from( - generator.descendantsTreeGenerator(root, { - generations: 3, - children: 3, - relatedEvents: 0, - relatedAlerts: 0, - percentTerminated: 100, - alwaysGenMaxChildrenPerNode: true, - }) - ); - - // because we requested the generator to always return the max children, there will always be at least 2 parents - const parents = findParents(children); - - // this represents the aggregation returned from elastic search - // each node in the tree should have 3 children, so if these values are greater than 3 there should be - // pagination cursors created for those children - const totals = { - [root.process.entity_id]: 100, - [entityId(parents[0])]: 10, - [entityId(parents[1])]: 0, - }; - - const helper = new ChildrenNodesHelper(root.process.entity_id); - helper.addChildren(totals, children); - const tree = helper.getNodes(); - expect(tree.nextChild).not.toBeNull(); - - let parent = findNode(tree, entityId(parents[0])); - expect(parent?.nextChild).not.toBeNull(); - parent = findNode(tree, entityId(parents[1])); - expect(parent?.nextChild).toBeNull(); - - tree.childNodes.forEach((node) => { + helper.addStartEvents(new Set([tree.origin.id]), childrenStartEvents); + helper.addLifecycleEvents(childrenEvents); + const childrenNodes = helper.getNodes(); + + // since we got all the nodes all the nextChild cursors should be null + for (const node of childrenNodes.childNodes) { + expect(node.nextChild).toBeUndefined(); + } + expect(childrenNodes.nextChild).not.toBeNull(); + + childrenNodes.childNodes.forEach((node) => { node.lifecycle.forEach((event) => { - expect(children.find((child) => child.event.id === eventId(event))).toEqual(event); + expect(childrenEvents.find((child) => child.event.id === eventId(event))).toEqual(event); }); }); }); it('builds the children response structure twice', () => { - const children = Array.from( - generator.descendantsTreeGenerator(root, { - generations: 3, - children: 3, - relatedEvents: 0, - relatedAlerts: 0, - percentTerminated: 100, - }) - ); - const helper = new ChildrenNodesHelper(root.process.entity_id); - helper.addChildren({}, children); + helper.addLifecycleEvents(childrenEvents); helper.getNodes(); - const tree = helper.getNodes(); - tree.childNodes.forEach((node) => { + const childrenNodes = helper.getNodes(); + childrenNodes.childNodes.forEach((node) => { node.lifecycle.forEach((event) => { - expect(children.find((child) => child.event.id === eventId(event))).toEqual(event); + expect(childrenEvents.find((child) => child.event.id === eventId(event))).toEqual(event); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index e60e5087c30a9e..01e356682ac478 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -8,35 +8,36 @@ import { entityId, parentEntityId, isProcessStart, + getAncestryAsArray, } from '../../../../../common/endpoint/models/event'; import { ResolverChildNode, ResolverEvent, ResolverChildren, } from '../../../../../common/endpoint/types'; -import { PaginationBuilder } from './pagination'; import { createChild } from './node'; +import { PaginationBuilder } from './pagination'; /** * This class helps construct the children structure when building a resolver tree. */ export class ChildrenNodesHelper { - private readonly cache: Map = new Map(); + private readonly entityToNodeCache: Map = new Map(); - constructor(private readonly rootID: string) { - this.cache.set(rootID, createChild(rootID)); + constructor(private readonly rootID: string, private readonly limit: number) { + this.entityToNodeCache.set(rootID, createChild(rootID)); } /** * Constructs a ResolverChildren response based on the children that were previously add. */ getNodes(): ResolverChildren { - const cacheCopy: Map = new Map(this.cache); + const cacheCopy: Map = new Map(this.entityToNodeCache); const rootNode = cacheCopy.get(this.rootID); let rootNextChild = null; if (rootNode) { - rootNextChild = rootNode.nextChild; + rootNextChild = rootNode.nextChild ?? null; } cacheCopy.delete(this.rootID); @@ -47,51 +48,131 @@ export class ChildrenNodesHelper { } /** - * Add children to the cache. - * - * @param totals a map of unique node IDs to total number of child nodes - * @param results events from a children query + * Get the entity_ids of the nodes that are cached. + */ + getEntityIDs(): string[] { + const cacheCopy: Map = new Map(this.entityToNodeCache); + cacheCopy.delete(this.rootID); + return Array.from(cacheCopy.keys()); + } + + /** + * Get the number of nodes that have been cached. */ - addChildren(totals: Record, results: ResolverEvent[]) { - const startEventsCache: Map = new Map(); + getNumNodes(): number { + // -1 because the root node is in the cache too + return this.entityToNodeCache.size - 1; + } - results.forEach((event) => { + /** + * Add lifecycle events (start, end, etc) to the cache. + * + * @param lifecycle an array of resolver lifecycle events for different process nodes returned from ES. + */ + addLifecycleEvents(lifecycle: ResolverEvent[]) { + for (const event of lifecycle) { const entityID = entityId(event); - const parentID = parentEntityId(event); - if (!entityID || !parentID) { - return; + if (entityID) { + const cachedChild = this.getOrCreateChildNode(entityID); + cachedChild.lifecycle.push(event); } + } + } - let cachedChild = this.cache.get(entityID); - if (!cachedChild) { - cachedChild = createChild(entityID); - this.cache.set(entityID, cachedChild); - } - cachedChild.lifecycle.push(event); + /** + * Add the start events for the nodes received from ES. Pagination cursors will be constructed based on the + * request limit and results returned. + * + * @param queriedNodes the entity_ids of the nodes that returned these start events + * @param startEvents an array of start events returned by ES + */ + addStartEvents(queriedNodes: Set, startEvents: ResolverEvent[]): Set | undefined { + let largestAncestryArray = 0; + const nodesToQueryNext: Map> = new Map(); + const nonLeafNodes: Set = new Set(); - if (isProcessStart(event)) { - let startEvents = startEventsCache.get(parentID); - if (startEvents === undefined) { - startEvents = []; - startEventsCache.set(parentID, startEvents); + const isDistantGrandchild = (event: ResolverEvent) => { + const ancestry = getAncestryAsArray(event); + return ancestry.length > 0 && queriedNodes.has(ancestry[ancestry.length - 1]); + }; + + for (const event of startEvents) { + const parentID = parentEntityId(event); + const entityID = entityId(event); + if (parentID && entityID && isProcessStart(event)) { + // don't actually add the start event to the node, because that'll be done in + // a different call + const childNode = this.getOrCreateChildNode(entityID); + + const ancestry = getAncestryAsArray(event); + // This is to handle the following unlikely but possible scenario: + // if an alert was generated by the kernel process (parent process of all other processes) then + // the direct children of that process would only have an ancestry array of [parent_kernel], a single value in the array. + // The children of those children would have two values in their array [direct parent, parent_kernel] + // we need to determine which nodes are the most distant grandchildren of the queriedNodes because those should + // be used for the next query if more nodes should be retrieved. To generally determine the most distant grandchildren + // we can use the last entry in the ancestry array because of its ordering. The problem with that is in the scenario above + // the direct children of parent_kernel will also meet that criteria even though they are not actually the most + // distant grandchildren. To get around that issue we'll bucket all the nodes by the size of their ancestry array + // and then only return the nodes in the largest bucket because those should be the most distant grandchildren + // from the queried nodes that were passed in. + if (ancestry.length > largestAncestryArray) { + largestAncestryArray = ancestry.length; + } + + // a grandchild must have an array of > 0 and have it's last parent be in the set of previously queried nodes + // this is one of the furthest descendants from the queried nodes + if (isDistantGrandchild(event)) { + let levelOfNodes = nodesToQueryNext.get(ancestry.length); + if (!levelOfNodes) { + levelOfNodes = new Set(); + nodesToQueryNext.set(ancestry.length, levelOfNodes); + } + levelOfNodes.add(entityID); + } else { + nonLeafNodes.add(childNode); } - startEvents.push(event); } - }); + } + + // we may not have received all the possible nodes so mark pagination for the query nodes + // we won't know if the non leaf nodes (non query nodes) have additional children so don't mark them + if (this.limit <= this.getNumNodes()) { + this.setPaginationForNodes(queriedNodes, startEvents); + return; + } + + // the non leaf nodes have received all their children so mark them as finished + for (const nonLeaf of nonLeafNodes.values()) { + nonLeaf.nextChild = null; + } - this.addChildrenPagination(startEventsCache, totals); + // we've received all the descendants of the previously queried node that we can get using it's ancestry array + // so mark those nodes as complete + for (const nodeEntityID of queriedNodes.values()) { + const node = this.entityToNodeCache.get(nodeEntityID); + if (node) { + node.nextChild = null; + } + } + return nodesToQueryNext.get(largestAncestryArray); } - private addChildrenPagination( - startEventsCache: Map, - totals: Record - ) { - Object.entries(totals).forEach(([parentID, total]) => { - const parentNode = this.cache.get(parentID); - const childrenStartEvents = startEventsCache.get(parentID); - if (parentNode && childrenStartEvents) { - parentNode.nextChild = PaginationBuilder.buildCursor(total, childrenStartEvents); + private setPaginationForNodes(nodes: Set, startEvents: ResolverEvent[]) { + for (const nodeEntityID of nodes.values()) { + const cachedNode = this.entityToNodeCache.get(nodeEntityID); + if (cachedNode) { + cachedNode.nextChild = PaginationBuilder.buildCursor(startEvents); } - }); + } + } + + private getOrCreateChildNode(entityID: string) { + let cachedChild = this.entityToNodeCache.get(entityID); + if (!cachedChild) { + cachedChild = createChild(entityID); + this.entityToNodeCache.set(entityID, cachedChild); + } + return cachedChild; } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts new file mode 100644 index 00000000000000..8aaf809405d631 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts @@ -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 { SearchResponse } from 'elasticsearch'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types'; +import { LifecycleQuery } from '../queries/lifecycle'; +import { QueryInfo } from '../queries/multi_searcher'; +import { SingleQueryHandler } from './fetch'; +import { ChildrenNodesHelper } from './children_helper'; +import { createChildren } from './node'; + +/** + * Returns the children of a resolver tree. + */ +export class ChildrenLifecycleQueryHandler implements SingleQueryHandler { + private lifecycle: ResolverChildren | undefined; + private readonly query: LifecycleQuery; + constructor( + private readonly childrenHelper: ChildrenNodesHelper, + indexPattern: string, + legacyEndpointID: string | undefined + ) { + this.query = new LifecycleQuery(indexPattern, legacyEndpointID); + } + + private handleResponse = (response: SearchResponse) => { + this.childrenHelper.addLifecycleEvents(this.query.formatResponse(response)); + this.lifecycle = this.childrenHelper.getNodes(); + }; + + /** + * Get the query for msearch. Once the results are set this will return undefined. + */ + nextQuery(): QueryInfo | undefined { + if (this.getResults()) { + return; + } + + return { + query: this.query, + ids: this.childrenHelper.getEntityIDs(), + handler: this.handleResponse, + }; + } + + /** + * Return the results from the search. + */ + getResults(): ResolverChildren | undefined { + return this.lifecycle; + } + + /** + * Perform a regular search and return the results. + * + * @param client the elasticsearch client + */ + async search(client: ILegacyScopedClusterClient) { + const results = this.getResults(); + if (results) { + return results; + } + + this.handleResponse(await this.query.search(client, this.childrenHelper.getEntityIDs())); + return this.getResults() || createChildren(); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_start_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_start_query_handler.ts new file mode 100644 index 00000000000000..1c741847207931 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_start_query_handler.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { ChildrenQuery } from '../queries/children'; +import { QueryInfo } from '../queries/multi_searcher'; +import { QueryHandler } from './fetch'; +import { ChildrenNodesHelper } from './children_helper'; +import { PaginationBuilder } from './pagination'; + +/** + * Retrieve the start lifecycle events for the children of a resolver tree. + * + * If using msearch you should loop over hasMore() because the results are limited to the size of the ancestry array. + */ +export class ChildrenStartQueryHandler implements QueryHandler { + private readonly childrenHelper: ChildrenNodesHelper; + private limitLeft: number; + private query: ChildrenQuery; + private nodesToQuery: Set; + + constructor( + private readonly limit: number, + entityID: string, + after: string | undefined, + private readonly indexPattern: string, + private readonly legacyEndpointID: string | undefined + ) { + this.query = new ChildrenQuery( + PaginationBuilder.createBuilder(limit, after), + indexPattern, + legacyEndpointID + ); + this.childrenHelper = new ChildrenNodesHelper(entityID, this.limit); + this.limitLeft = this.limit; + this.nodesToQuery = new Set([entityID]); + } + + private setNoMore() { + this.nodesToQuery = new Set(); + this.limitLeft = 0; + } + + private handleResponse = (response: SearchResponse) => { + const results = this.query.formatResponse(response); + this.nodesToQuery = this.childrenHelper.addStartEvents(this.nodesToQuery, results) ?? new Set(); + + if (results.length === 0) { + this.setNoMore(); + return; + } + + this.limitLeft = this.limit - this.childrenHelper.getNumNodes(); + this.query = new ChildrenQuery( + PaginationBuilder.createBuilder(this.limitLeft), + this.indexPattern, + this.legacyEndpointID + ); + }; + + /** + * Check if there are more results to retrieve based on the limit that was passed in. + */ + hasMore(): boolean { + return this.limitLeft > 0 && this.nodesToQuery.size > 0; + } + + /** + * Get a query to retrieve the next set of results. + */ + nextQuery(): QueryInfo | undefined { + if (this.hasMore()) { + return { + query: this.query, + // This should never be undefined because the check above + ids: Array.from(this.nodesToQuery.values()), + handler: this.handleResponse, + }; + } + } + + /** + * Get the cached results from the ES responses. + */ + getResults(): ChildrenNodesHelper { + return this.childrenHelper; + } + + /** + * Perform a regular search and return the helper. + * + * @param client the elasticsearch client + */ + async search(client: ILegacyScopedClusterClient) { + while (this.hasMore()) { + const info = this.nextQuery(); + if (!info) { + break; + } + this.handleResponse(await this.query.search(client, info.ids)); + } + return this.getResults(); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts new file mode 100644 index 00000000000000..849dbc25fe4db9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { ResolverRelatedEvents, ResolverEvent } from '../../../../../common/endpoint/types'; +import { createRelatedEvents } from './node'; +import { EventsQuery } from '../queries/events'; +import { PaginationBuilder } from './pagination'; +import { QueryInfo } from '../queries/multi_searcher'; +import { SingleQueryHandler } from './fetch'; + +/** + * This retrieves the related events for the origin node of a resolver tree. + */ +export class RelatedEventsQueryHandler implements SingleQueryHandler { + private relatedEvents: ResolverRelatedEvents | undefined; + private readonly query: EventsQuery; + constructor( + private readonly limit: number, + private readonly entityID: string, + after: string | undefined, + indexPattern: string, + legacyEndpointID: string | undefined + ) { + this.query = new EventsQuery( + PaginationBuilder.createBuilder(limit, after), + indexPattern, + legacyEndpointID + ); + } + + private handleResponse = (response: SearchResponse) => { + const results = this.query.formatResponse(response); + this.relatedEvents = createRelatedEvents( + this.entityID, + results, + PaginationBuilder.buildCursorRequestLimit(this.limit, results) + ); + }; + + /** + * Get a query to use in a msearch. + */ + nextQuery(): QueryInfo | undefined { + if (this.getResults()) { + return; + } + + return { + query: this.query, + ids: this.entityID, + handler: this.handleResponse, + }; + } + + /** + * Get the results after an msearch. + */ + getResults() { + return this.relatedEvents; + } + + /** + * Perform a normal search and return the related events results. + * + * @param client the elasticsearch client + */ + async search(client: ILegacyScopedClusterClient) { + const results = this.getResults(); + if (results) { + return results; + } + + this.handleResponse(await this.query.search(client, this.entityID)); + return this.getResults() ?? createRelatedEvents(this.entityID); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index 1a532c54c7d5d1..feb165c308a91c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -11,22 +11,61 @@ import { ResolverAncestry, ResolverRelatedAlerts, ResolverLifecycleNode, - ResolverEvent, } from '../../../../../common/endpoint/types'; -import { - entityId, - ancestryArray, - parentEntityId, -} from '../../../../../common/endpoint/models/event'; -import { PaginationBuilder } from './pagination'; import { Tree } from './tree'; import { LifecycleQuery } from '../queries/lifecycle'; -import { ChildrenQuery } from '../queries/children'; -import { EventsQuery } from '../queries/events'; import { StatsQuery } from '../queries/stats'; -import { createAncestry, createRelatedEvents, createLifecycle, createRelatedAlerts } from './node'; -import { ChildrenNodesHelper } from './children_helper'; -import { AlertsQuery } from '../queries/alerts'; +import { createLifecycle } from './node'; +import { MultiSearcher, QueryInfo } from '../queries/multi_searcher'; +import { AncestryQueryHandler } from './ancestry_query_handler'; +import { RelatedEventsQueryHandler } from './events_query_handler'; +import { RelatedAlertsQueryHandler } from './alerts_query_handler'; +import { ChildrenStartQueryHandler } from './children_start_query_handler'; +import { ChildrenLifecycleQueryHandler } from './children_lifecycle_query_handler'; +import { LifecycleQueryHandler } from './lifecycle_query_handler'; + +/** + * The query parameters passed in from the request. These define the limits for the ES requests for retrieving the + * resolver tree. + */ +export interface TreeOptions { + children: number; + ancestors: number; + events: number; + alerts: number; + afterAlert?: string; + afterEvent?: string; + afterChild?: string; +} + +interface QueryBuilder { + nextQuery(): QueryInfo | undefined; +} + +/** + * This interface defines the contract for a query handler that will only be used once in an msearch call. + */ +export interface SingleQueryHandler extends QueryBuilder { + /** + * This method returns the results if the query has been used in an msearch call or undefined if not. + */ + getResults(): T | undefined; + /** + * Do a regular search instead of msearch. + * @param client the elasticsearch client + */ + search(client: ILegacyScopedClusterClient): Promise; +} + +/** + * This interface defines the contract for a query handler that can be used multiple times by msearch. + */ +export interface QueryHandler extends SingleQueryHandler { + /** + * Returns whether additional msearch are required to retrieve the rest of the expected data from ES. + */ + hasMore(): boolean; +} /** * Handles retrieving nodes of a resolver tree. @@ -52,46 +91,138 @@ export class Fetcher { private readonly endpointID?: string ) {} + /** + * This method retrieves the resolver tree starting from the `id` during construction of the class. + * + * @param options the options for retrieving the structure of the tree. + */ + public async tree(options: TreeOptions) { + const addQueryToList = (queryHandler: QueryBuilder, queries: QueryInfo[]) => { + const queryInfo = queryHandler.nextQuery(); + if (queryInfo !== undefined) { + queries.push(queryInfo); + } + }; + + const originHandler = new LifecycleQueryHandler( + this.id, + this.eventsIndexPattern, + this.endpointID + ); + + const eventsHandler = new RelatedEventsQueryHandler( + options.events, + this.id, + options.afterEvent, + this.eventsIndexPattern, + this.endpointID + ); + + const alertsHandler = new RelatedAlertsQueryHandler( + options.alerts, + this.id, + options.afterAlert, + this.alertsIndexPattern, + this.endpointID + ); + + // we need to get the start events first because the API request defines how many nodes to return and we don't want + // to count or limit ourselves based on the other lifecycle events (end, etc) + const childrenHandler = new ChildrenStartQueryHandler( + options.children, + this.id, + options.afterChild, + this.eventsIndexPattern, + this.endpointID + ); + + const msearch = new MultiSearcher(this.client); + + let queries: QueryInfo[] = []; + addQueryToList(eventsHandler, queries); + addQueryToList(alertsHandler, queries); + addQueryToList(childrenHandler, queries); + addQueryToList(originHandler, queries); + + // get the related events, related alerts, the first pass of children start events, and the origin node + // the origin node is needed so we can get the ancestry array for the additional ancestor calls + await msearch.search(queries); + + const ancestryHandler = new AncestryQueryHandler( + options.ancestors, + this.eventsIndexPattern, + this.endpointID, + originHandler.getResults() + ); + + // get the remaining ancestors and children start events + while (ancestryHandler.hasMore() || childrenHandler.hasMore()) { + queries = []; + addQueryToList(ancestryHandler, queries); + addQueryToList(childrenHandler, queries); + await msearch.search(queries); + } + + const childrenTotalsHelper = childrenHandler.getResults(); + + const childrenLifecycleHandler = new ChildrenLifecycleQueryHandler( + childrenTotalsHelper, + this.eventsIndexPattern, + this.endpointID + ); + + // now that we have all the start events get the full lifecycle nodes + childrenLifecycleHandler.search(this.client); + + const tree = new Tree(this.id, { + ancestry: ancestryHandler.getResults(), + relatedEvents: eventsHandler.getResults(), + relatedAlerts: alertsHandler.getResults(), + children: childrenLifecycleHandler.getResults(), + }); + + // add the stats to the tree + return this.stats(tree); + } + /** * Retrieves the ancestor nodes for the resolver tree. * * @param limit upper limit of ancestors to retrieve */ public async ancestors(limit: number): Promise { - const ancestryInfo = createAncestry(); const originNode = await this.getNode(this.id); - if (originNode) { - ancestryInfo.ancestors.push(originNode); - // If the request is only for the origin node then set next to its parent - ancestryInfo.nextAncestor = parentEntityId(originNode.lifecycle[0]) || null; - await this.doAncestors( - // limit the ancestors we're looking for to the number of levels - // the array could be up to length 20 but that could change - Fetcher.getAncestryAsArray(originNode.lifecycle[0]).slice(0, limit), - limit, - ancestryInfo - ); - } - return ancestryInfo; + const ancestryHandler = new AncestryQueryHandler( + limit, + this.eventsIndexPattern, + this.endpointID, + originNode + ); + return ancestryHandler.search(this.client); } /** * Retrieves the children nodes for the resolver tree. * * @param limit the number of children to retrieve for a single level - * @param generations number of levels to return * @param after a cursor to use as the starting point for retrieving children */ - public async children( - limit: number, - generations: number, - after?: string - ): Promise { - const helper = new ChildrenNodesHelper(this.id); - - await this.doChildren(helper, [this.id], limit, generations, after); + public async children(limit: number, after?: string): Promise { + const childrenHandler = new ChildrenStartQueryHandler( + limit, + this.id, + after, + this.eventsIndexPattern, + this.endpointID + ); + const helper = await childrenHandler.search(this.client); + const childrenLifecycleHandler = new ChildrenLifecycleQueryHandler( + helper, + this.eventsIndexPattern, + this.endpointID + ); - return helper.getNodes(); + return childrenLifecycleHandler.search(this.client); } /** @@ -101,7 +232,15 @@ export class Fetcher { * @param after a cursor to use as the starting point for retrieving related events */ public async events(limit: number, after?: string): Promise { - return this.doEvents(limit, after); + const eventsHandler = new RelatedEventsQueryHandler( + limit, + this.id, + after, + this.eventsIndexPattern, + this.endpointID + ); + + return eventsHandler.search(this.client); } /** @@ -111,26 +250,15 @@ export class Fetcher { * @param after a cursor to use as the starting point for retrieving alerts */ public async alerts(limit: number, after?: string): Promise { - const query = new AlertsQuery( - PaginationBuilder.createBuilder(limit, after), + const alertsHandler = new RelatedAlertsQueryHandler( + limit, + this.id, + after, this.alertsIndexPattern, this.endpointID ); - const { totals, results } = await query.search(this.client, this.id); - if (results.length === 0) { - // return an empty set of results - return createRelatedAlerts(this.id); - } - if (!totals[this.id]) { - throw new Error(`Could not find the totals for related events entity_id: ${this.id}`); - } - - return createRelatedAlerts( - this.id, - results, - PaginationBuilder.buildCursor(totals[this.id], results) - ); + return alertsHandler.search(this.client); } /** @@ -145,7 +273,7 @@ export class Fetcher { private async getNode(entityID: string): Promise { const query = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); - const results = await query.search(this.client, entityID); + const results = await query.searchAndFormat(this.client, entityID); if (results.length === 0) { return; } @@ -153,125 +281,13 @@ export class Fetcher { return createLifecycle(entityID, results); } - private static getAncestryAsArray(event: ResolverEvent): string[] { - const ancestors = ancestryArray(event); - if (ancestors) { - return ancestors; - } - - const parentID = parentEntityId(event); - if (parentID) { - return [parentID]; - } - - return []; - } - - private async doAncestors( - ancestors: string[], - levels: number, - ancestorInfo: ResolverAncestry - ): Promise { - if (levels <= 0) { - return; - } - - const query = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); - const results = await query.search(this.client, ancestors); - - if (results.length === 0) { - ancestorInfo.nextAncestor = null; - return; - } - - // bucket the start and end events together for a single node - const ancestryNodes = results.reduce( - (nodes: Map, ancestorEvent: ResolverEvent) => { - const nodeId = entityId(ancestorEvent); - let node = nodes.get(nodeId); - if (!node) { - node = createLifecycle(nodeId, []); - } - - node.lifecycle.push(ancestorEvent); - return nodes.set(nodeId, node); - }, - new Map() - ); - - // the order of this array is going to be weird, it will look like this - // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent] - ancestorInfo.ancestors.push(...ancestryNodes.values()); - ancestorInfo.nextAncestor = parentEntityId(results[0]) || null; - const levelsLeft = levels - ancestryNodes.size; - // the results come back in ascending order on timestamp so the first entry in the - // results should be the further ancestor (most distant grandparent) - const next = Fetcher.getAncestryAsArray(results[0]).slice(0, levelsLeft); - // the ancestry array currently only holds up to 20 values but we can't rely on that so keep recursing - await this.doAncestors(next, levelsLeft, ancestorInfo); - } - - private async doEvents(limit: number, after?: string) { - const query = new EventsQuery( - PaginationBuilder.createBuilder(limit, after), - this.eventsIndexPattern, - this.endpointID - ); - - const { totals, results } = await query.search(this.client, this.id); - if (results.length === 0) { - // return an empty set of results - return createRelatedEvents(this.id); - } - if (!totals[this.id]) { - throw new Error(`Could not find the totals for related events entity_id: ${this.id}`); - } - - return createRelatedEvents( - this.id, - results, - PaginationBuilder.buildCursor(totals[this.id], results) - ); - } - - private async doChildren( - cache: ChildrenNodesHelper, - ids: string[], - limit: number, - levels: number, - after?: string - ) { - if (levels === 0 || ids.length === 0) { - return; - } - - const childrenQuery = new ChildrenQuery( - PaginationBuilder.createBuilder(limit, after), - this.eventsIndexPattern, - this.endpointID - ); - const lifecycleQuery = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); - - const { totals, results } = await childrenQuery.search(this.client, ids); - if (results.length === 0) { - return; - } - - const childIDs = results.map(entityId); - const children = await lifecycleQuery.search(this.client, childIDs); - - cache.addChildren(totals, children); - - await this.doChildren(cache, childIDs, limit, levels - 1); - } - private async doStats(tree: Tree) { const statsQuery = new StatsQuery( [this.eventsIndexPattern, this.alertsIndexPattern], this.endpointID ); const ids = tree.ids(); - const res = await statsQuery.search(this.client, ids); + const res = await statsQuery.searchAndFormat(this.client, ids); const alerts = res.alerts; const events = res.events; ids.forEach((id) => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts new file mode 100644 index 00000000000000..ab0501e099490b --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.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 { SearchResponse } from 'elasticsearch'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { ResolverEvent, ResolverLifecycleNode } from '../../../../../common/endpoint/types'; +import { LifecycleQuery } from '../queries/lifecycle'; +import { QueryInfo } from '../queries/multi_searcher'; +import { SingleQueryHandler } from './fetch'; +import { createLifecycle } from './node'; + +/** + * Retrieve the lifecycle events for a node. + */ +export class LifecycleQueryHandler implements SingleQueryHandler { + private lifecycle: ResolverLifecycleNode | undefined; + private readonly query: LifecycleQuery; + constructor( + private readonly entityID: string, + indexPattern: string, + legacyEndpointID: string | undefined + ) { + this.query = new LifecycleQuery(indexPattern, legacyEndpointID); + } + + private handleResponse = (response: SearchResponse) => { + const results = this.query.formatResponse(response); + if (results.length !== 0) { + this.lifecycle = createLifecycle(this.entityID, results); + } + }; + + /** + * Build the query for retrieving the lifecycle events. This will return undefined once the results have been found. + */ + nextQuery(): QueryInfo | undefined { + if (this.getResults()) { + return; + } + + return { + query: this.query, + ids: this.entityID, + handler: this.handleResponse, + }; + } + + /** + * Get the results from the msearch. + */ + getResults(): ResolverLifecycleNode | undefined { + return this.lifecycle; + } + + /** + * Do a regular search and return the results. + * + * @param client the elasticsearch client. + */ + async search(client: ILegacyScopedClusterClient) { + const results = this.getResults(); + if (results) { + return results; + } + + this.handleResponse(await this.query.search(client, this.entityID)); + return this.getResults() ?? createLifecycle(this.entityID, []); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index 57a2ebfcc17929..98180885faf052 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -12,6 +12,7 @@ import { ResolverTree, ResolverChildNode, ResolverRelatedAlerts, + ResolverChildren, } from '../../../../../common/endpoint/types'; /** @@ -53,7 +54,6 @@ export function createChild(entityID: string): ResolverChildNode { const lifecycle = createLifecycle(entityID, []); return { ...lifecycle, - nextChild: null, }; } @@ -77,6 +77,19 @@ export function createLifecycle( return { entityID, lifecycle }; } +/** + * Creates a resolver children response. + * + * @param nodes the child nodes to add to the ResolverChildren response + * @param nextChild the cursor for the response + */ +export function createChildren( + nodes: ResolverChildNode[] = [], + nextChild: string | null = null +): ResolverChildren { + return { childNodes: nodes, nextChild }; +} + /** * Creates an empty `Tree` response structure that the tree handler would return * diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts index 74e4e252861e6b..4daa45aec2a740 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts @@ -18,20 +18,20 @@ describe('Pagination', () => { const root = generator.generateEvent(); const events = Array.from(generator.relatedEventsGenerator(root, 5)); - it('does not build a cursor when all events are present', () => { - expect(PaginationBuilder.buildCursor(0, events)).toBeNull(); + it('does build a cursor when received the same number of events as was requested', () => { + expect(PaginationBuilder.buildCursorRequestLimit(4, events)).not.toBeNull(); }); - it('creates a cursor when not all events are present', () => { - expect(PaginationBuilder.buildCursor(events.length + 1, events)).not.toBeNull(); + it('does not create a cursor when the number of events received is less than the amount requested', () => { + expect(PaginationBuilder.buildCursorRequestLimit(events.length + 1, events)).toBeNull(); }); it('creates a cursor with the right information', () => { - const cursor = PaginationBuilder.buildCursor(events.length + 1, events); + const cursor = PaginationBuilder.buildCursorRequestLimit(events.length, events); expect(cursor).not.toBeNull(); // we are guaranteed that the cursor won't be null from the check above const builder = PaginationBuilder.createBuilder(0, cursor!); - const fields = builder.buildQueryFields(0, '', ''); + const fields = builder.buildQueryFields(''); expect(fields.search_after).toStrictEqual(getSearchAfterInfo(events)); }); }); @@ -39,30 +39,8 @@ describe('Pagination', () => { describe('pagination builder', () => { it('does not include the search after information when no cursor is provided', () => { const builder = PaginationBuilder.createBuilder(100); - const fields = builder.buildQueryFields(1, '', ''); + const fields = builder.buildQueryFields(''); expect(fields).not.toHaveProperty('search_after'); }); - - it('returns no results when the aggregation does not exist in the response', () => { - expect(PaginationBuilder.getTotals()).toStrictEqual({}); - }); - - it('constructs the totals from the aggregation results', () => { - const agg = { - totals: { - buckets: [ - { - key: 'awesome', - doc_count: 5, - }, - { - key: 'soup', - doc_count: 1, - }, - ], - }, - }; - expect(PaginationBuilder.getTotals(agg)).toStrictEqual({ awesome: 5, soup: 1 }); - }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 61cb5bdb8f1465..2b107ab1b6db4d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -8,41 +8,11 @@ import { ResolverEvent } from '../../../../../common/endpoint/types'; import { eventId } from '../../../../../common/endpoint/models/event'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; -/** - * Represents a single result bucket of an aggregation - */ -export interface AggBucket { - key: string; - doc_count: number; -} - -interface TotalsAggregation { - totals?: { - buckets?: AggBucket[]; - }; -} - interface PaginationCursor { timestamp: number; eventID: string; } -/** - * The result structure of a query that leverages pagination. This includes totals that can be used to determine if - * additional nodes exist and additional queries need to be made to retrieve the nodes. - */ -export interface PaginatedResults { - /** - * Resulting events returned from the query. - */ - results: ResolverEvent[]; - /** - * Mapping of unique ID to total number of events that exist in ES. The events this references is scoped to the events - * that the query is searching for. - */ - totals: Record; -} - /** * This class handles constructing pagination cursors that resolver can use to return additional events in subsequent * queries. It also constructs an aggregation query to determine the totals for other queries. This class should be used @@ -83,19 +53,28 @@ export class PaginationBuilder { } /** - * Constructs a cursor to use in subsequent queries to retrieve the next set of results. + * Construct a cursor to use in subsequent queries. * - * @param total the total events that exist in ES scoped for a particular query. * @param results the events that were returned by the ES query */ - static buildCursor(total: number, results: ResolverEvent[]): string | null { - if (total > results.length && results.length > 0) { - const lastResult = results[results.length - 1]; - const cursor = { - timestamp: lastResult['@timestamp'], - eventID: eventId(lastResult), - }; - return PaginationBuilder.urlEncodeCursor(cursor); + static buildCursor(results: ResolverEvent[]): string | null { + const lastResult = results[results.length - 1]; + const cursor = { + timestamp: lastResult['@timestamp'], + eventID: eventId(lastResult), + }; + return PaginationBuilder.urlEncodeCursor(cursor); + } + + /** + * Constructs a cursor if the requested limit has not been met. + * + * @param requestLimit the request limit for a query. + * @param results the events that were returned by the ES query + */ + static buildCursorRequestLimit(requestLimit: number, results: ResolverEvent[]): string | null { + if (requestLimit <= results.length && results.length > 0) { + return PaginationBuilder.buildCursor(results); } return null; } @@ -124,45 +103,16 @@ export class PaginationBuilder { /** * Creates an object for adding the pagination fields to a query * - * @param numTerms number of unique IDs that are being search for in this query * @param tiebreaker a unique field to use as the tiebreaker for the search_after - * @param aggregator the field that specifies a unique ID per event (e.g. entity_id) - * @param aggs other aggregations being used with this query * @returns an object containing the pagination information */ - buildQueryFields( - numTerms: number, - tiebreaker: string, - aggregator: string, - aggs: JsonObject = {} - ): JsonObject { + buildQueryFields(tiebreaker: string): JsonObject { const fields: JsonObject = {}; fields.sort = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; - fields.aggs = { ...aggs, totals: { terms: { field: aggregator, size: numTerms } } }; fields.size = this.size; if (this.timestamp && this.eventID) { fields.search_after = [this.timestamp, this.eventID] as Array; } return fields; } - - /** - * Returns the totals found for the specified query - * - * @param aggregations the aggregation field from the ES response - * @returns a mapping of unique ID (e.g. entity_ids) to totals found for those IDs - */ - static getTotals(aggregations?: TotalsAggregation): Record { - if (!aggregations?.totals?.buckets) { - return {}; - } - - return aggregations?.totals?.buckets?.reduce( - (cumulative: Record, bucket: AggBucket) => ({ - ...cumulative, - [bucket.key]: bucket.doc_count, - }), - {} - ); - } } diff --git a/x-pack/plugins/snapshot_restore/README.md b/x-pack/plugins/snapshot_restore/README.md new file mode 100644 index 00000000000000..e11483785e958d --- /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/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts index 2f7b75dfba57e5..69d1423f5f8fb6 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts @@ -3,12 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import './mocks'; import { setup as homeSetup } from './home.helpers'; import { setup as repositoryAddSetup } from './repository_add.helpers'; import { setup as repositoryEditSetup } from './repository_edit.helpers'; import { setup as policyAddSetup } from './policy_add.helpers'; import { setup as policyEditSetup } from './policy_edit.helpers'; +import { setup as restoreSnapshotSetup } from './restore_snapshot.helpers'; export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../test_utils'; @@ -20,4 +21,5 @@ export const pageHelpers = { repositoryEdit: { setup: repositoryEditSetup }, policyAdd: { setup: policyAddSetup }, policyEdit: { setup: policyEditSetup }, + restoreSnapshot: { setup: restoreSnapshotSetup }, }; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/mocks.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/mocks.tsx new file mode 100644 index 00000000000000..fc02452e373088 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/mocks.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +/* + * Mocking AutoSizer of the react-virtualized because it does not render children in JS DOM. + * This seems related to not being able to properly discover height and width. + */ +jest.mock('react-virtualized', () => { + const original = jest.requireActual('react-virtualized'); + + return { + ...original, + AutoSizer: ({ children }: { children: any }) => ( +
{children({ height: 500, width: 500 })}
+ ), + }; +}); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts index 131969b997b532..a3ab829ab642cd 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts @@ -41,6 +41,8 @@ export type PolicyFormTestSubjects = | 'allIndicesToggle' | 'backButton' | 'deselectIndicesLink' + | 'allDataStreamsToggle' + | 'deselectDataStreamLink' | 'expireAfterValueInput' | 'expireAfterUnitSelect' | 'ignoreUnavailableIndicesToggle' @@ -53,4 +55,5 @@ export type PolicyFormTestSubjects = | 'selectIndicesLink' | 'showAdvancedCronLink' | 'snapshotNameInput' + | 'dataStreamBadge' | 'submitButton'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts new file mode 100644 index 00000000000000..0cfb6fbc979755 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; +import { RestoreSnapshot } from '../../../public/application/sections/restore_snapshot'; +import { WithAppDependencies } from './setup_environment'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: ['/add_policy'], + componentRoutePath: '/add_policy', + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed( + WithAppDependencies(RestoreSnapshot), + testBedConfig +); + +const setupActions = (testBed: TestBed) => { + const { find } = testBed; + return { + findDataStreamCallout() { + return find('dataStreamWarningCallOut'); + }, + }; +}; + +type Actions = ReturnType; + +export type RestoreSnapshotTestBed = TestBed & { + actions: Actions; +}; + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: setupActions(testBed), + }; +}; + +export type RestoreSnapshotFormTestSubject = + | 'snapshotRestoreStepLogistics' + | 'dataStreamWarningCallOut'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx index c4f4876b8a1cdb..e3c0ab0be9bd23 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx @@ -64,6 +64,14 @@ export const setupEnvironment = () => { }; }; +/** + * Suppress error messages about Worker not being available in JS DOM. + */ +(window as any).Worker = function Worker() { + this.postMessage = () => {}; + this.terminate = () => {}; +}; + export const WithAppDependencies = (Comp: any) => (props: any) => ( diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index a8e6e976bb16da..17a745fafcc266 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -3,11 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +// import helpers first, this also sets up the mocks +import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; + import { act } from 'react-dom/test-utils'; import * as fixtures from '../../test/fixtures'; -import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; import { PolicyFormTestBed } from './helpers/policy_form.helpers'; import { DEFAULT_POLICY_SCHEDULE } from '../../public/application/constants'; @@ -37,7 +40,10 @@ describe('', () => { describe('on component mount', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [repository] }); - httpRequestsMockHelpers.setLoadIndicesResponse({ indices: ['my_index'] }); + httpRequestsMockHelpers.setLoadIndicesResponse({ + indices: ['my_index'], + dataStreams: ['my_data_stream', 'my_other_data_stream'], + }); testBed = await setup(); await nextTick(); @@ -96,7 +102,7 @@ describe('', () => { actions.clickNextButton(); }); - test('should require at least one index', async () => { + test('should require at least one index if no data streams are provided', async () => { const { find, form, component } = testBed; await act(async () => { @@ -109,7 +115,22 @@ describe('', () => { // Deselect all indices from list find('deselectIndicesLink').simulate('click'); - expect(form.getErrorsMessages()).toEqual(['You must select at least one index.']); + expect(form.getErrorsMessages()).toEqual([ + 'You must select at least one data stream or index.', + ]); + }); + + test('should correctly indicate data streams with a badge', async () => { + const { find, component, form } = testBed; + + await act(async () => { + // Toggle "All indices" switch + form.toggleEuiSwitch('allIndicesToggle', false); + await nextTick(); + }); + component.update(); + + expect(find('dataStreamBadge').length).toBe(2); }); }); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index 297741755e88bf..7eec80890ca86a 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -35,7 +35,10 @@ describe('', () => { describe('on component mount', () => { beforeEach(async () => { httpRequestsMockHelpers.setGetPolicyResponse({ policy: POLICY_EDIT }); - httpRequestsMockHelpers.setLoadIndicesResponse({ indices: ['my_index'] }); + httpRequestsMockHelpers.setLoadIndicesResponse({ + indices: ['my_index'], + dataStreams: ['my_data_stream'], + }); httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [{ name: POLICY_EDIT.repository }], }); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts new file mode 100644 index 00000000000000..17d714c07429f4 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { nextTick, pageHelpers, setupEnvironment } from './helpers'; +import { RestoreSnapshotTestBed } from './helpers/restore_snapshot.helpers'; +import * as fixtures from '../../test/fixtures'; + +const { + restoreSnapshot: { setup }, +} = pageHelpers; + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: RestoreSnapshotTestBed; + + afterAll(() => { + server.restore(); + }); + describe('with data streams', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); + testBed = await setup(); + await nextTick(); + testBed.component.update(); + }); + + it('shows the data streams warning when the snapshot has data streams', () => { + const { exists } = testBed; + expect(exists('dataStreamWarningCallOut')).toBe(true); + }); + }); + + describe('without data streams', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot({ totalDataStreams: 0 })); + testBed = await setup(); + await nextTick(); + testBed.component.update(); + }); + + it('hides the data streams warning when the snapshot has data streams', () => { + const { exists } = testBed; + expect(exists('dataStreamWarningCallOut')).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/common/lib/index.ts b/x-pack/plugins/snapshot_restore/common/lib/index.ts index 579dae02659392..eaec8054a93abc 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/index.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/index.ts @@ -16,3 +16,5 @@ export { serializeSnapshotRetention, } from './snapshot_serialization'; export { deserializePolicy, serializePolicy } from './policy_serialization'; +export { csvToArray } from './utils'; +export { isDataStreamBackingIndex } from './is_data_stream_backing_index'; diff --git a/x-pack/plugins/snapshot_restore/common/lib/is_data_stream_backing_index.ts b/x-pack/plugins/snapshot_restore/common/lib/is_data_stream_backing_index.ts new file mode 100644 index 00000000000000..3b937670362f7b --- /dev/null +++ b/x-pack/plugins/snapshot_restore/common/lib/is_data_stream_backing_index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * @remark + * WARNING! + * + * This is a very hacky way of determining whether an index is a backing index. + * + * We only do this so that we can show users during a snapshot restore workflow + * that an index is part of a data stream. At the moment there is no way for us + * to get this information from the snapshot itself, even though it contains the + * metadata for the data stream that information is fully opaque to us until after + * we have done the snapshot restore. + * + * Issue for tracking this discussion here: https://github.com/elastic/elasticsearch/issues/58890 + */ +export const isDataStreamBackingIndex = (indexName: string) => { + return indexName.startsWith('.ds'); +}; diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts index 298fc235fd9cc7..473a3392deb3ee 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts @@ -97,6 +97,7 @@ describe('deserializeSnapshotDetails', () => { version: 'version', // Indices are sorted. indices: ['index1', 'index2', 'index3'], + dataStreams: [], includeGlobalState: false, // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. indexFailures: [ diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts index a636cc1f6326ec..a85b49430eecd5 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts @@ -17,6 +17,8 @@ import { import { deserializeTime, serializeTime } from './time_serialization'; +import { csvToArray } from './utils'; + export function deserializeSnapshotDetails( repository: string, snapshotDetailsEs: SnapshotDetailsEs, @@ -33,6 +35,7 @@ export function deserializeSnapshotDetails( version_id: versionId, version, indices = [], + data_streams: dataStreams = [], include_global_state: includeGlobalState, state, start_time: startTime, @@ -77,6 +80,7 @@ export function deserializeSnapshotDetails( versionId, version, indices: [...indices].sort(), + dataStreams: [...dataStreams].sort(), includeGlobalState, state, startTime, @@ -127,8 +131,10 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): SnapshotConfigEs { const { indices, ignoreUnavailable, includeGlobalState, partial, metadata } = snapshotConfig; + const indicesArray = csvToArray(indices); + const snapshotConfigEs: SnapshotConfigEs = { - indices, + indices: indicesArray, ignore_unavailable: ignoreUnavailable, include_global_state: includeGlobalState, partial, diff --git a/x-pack/plugins/snapshot_restore/common/lib/utils.ts b/x-pack/plugins/snapshot_restore/common/lib/utils.ts new file mode 100644 index 00000000000000..96eb7cb6908d89 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/common/lib/utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const csvToArray = (indices?: string | string[]): string[] => { + return indices && Array.isArray(indices) + ? indices + : typeof indices === 'string' + ? indices.split(',') + : []; +}; diff --git a/x-pack/plugins/snapshot_restore/common/types/index.ts b/x-pack/plugins/snapshot_restore/common/types/index.ts index d52584ca737a2a..a12ae904cfee83 100644 --- a/x-pack/plugins/snapshot_restore/common/types/index.ts +++ b/x-pack/plugins/snapshot_restore/common/types/index.ts @@ -8,3 +8,4 @@ export * from './repository'; export * from './snapshot'; export * from './restore'; export * from './policy'; +export * from './indices'; diff --git a/x-pack/plugins/apm/typings/lodash.mean.d.ts b/x-pack/plugins/snapshot_restore/common/types/indices.ts similarity index 67% rename from x-pack/plugins/apm/typings/lodash.mean.d.ts rename to x-pack/plugins/snapshot_restore/common/types/indices.ts index 0b9ca3f6914cb9..5e4f2b5fdc1678 100644 --- a/x-pack/plugins/apm/typings/lodash.mean.d.ts +++ b/x-pack/plugins/snapshot_restore/common/types/indices.ts @@ -4,7 +4,7 @@ * 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 interface PolicyIndicesResponse { + indices: string[]; + dataStreams: string[]; } diff --git a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts index a46f5c7921bfe0..1ff058e1553857 100644 --- a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts @@ -30,6 +30,7 @@ export interface SnapshotDetails { versionId: number; version: string; indices: string[]; + dataStreams: string[]; includeGlobalState: boolean; state: string; /** e.g. '2019-04-05T21:56:40.438Z' */ @@ -52,6 +53,7 @@ export interface SnapshotDetailsEs { version_id: number; version: string; indices: string[]; + data_streams?: string[]; include_global_state: boolean; state: string; /** e.g. '2019-04-05T21:56:40.438Z' */ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_indices_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_indices_list.tsx deleted file mode 100644 index 1d8ee726f4cc79..00000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_indices_list.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiLink, EuiIcon, EuiText, EuiSpacer } from '@elastic/eui'; -interface Props { - indices: string[] | string | undefined; -} - -export const CollapsibleIndicesList: React.FunctionComponent = ({ indices }) => { - const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState(false); - const displayIndices = indices - ? typeof indices === 'string' - ? indices.split(',') - : indices - : undefined; - const hiddenIndicesCount = - displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0; - return ( - <> - {displayIndices ? ( - <> - -
    - {(isShowingFullIndicesList ? displayIndices : [...displayIndices].splice(0, 10)).map( - (index) => ( -
  • - - {index} - -
  • - ) - )} -
-
- {hiddenIndicesCount ? ( - <> - - - isShowingFullIndicesList - ? setIsShowingFullIndicesList(false) - : setIsShowingFullIndicesList(true) - } - > - {isShowingFullIndicesList ? ( - - ) : ( - - )}{' '} - - - - ) : null} - - ) : ( - - )} - - ); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx new file mode 100644 index 00000000000000..ce1bd7c8d6e45f --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiLink, EuiIcon, EuiText, EuiSpacer } from '@elastic/eui'; + +import { useCollapsibleList } from './use_collapsible_list'; + +interface Props { + dataStreams: string[] | string | undefined; +} + +export const CollapsibleDataStreamsList: React.FunctionComponent = ({ dataStreams }) => { + const { isShowingFullList, setIsShowingFullList, items, hiddenItemsCount } = useCollapsibleList({ + items: dataStreams, + }); + + return items === 'all' ? ( + + ) : ( + <> + +
    + {items.map((dataStream) => ( +
  • + + {dataStream} + +
  • + ))} +
+
+ {hiddenItemsCount ? ( + <> + + + isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true) + } + > + {isShowingFullList ? ( + + ) : ( + + )}{' '} + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx new file mode 100644 index 00000000000000..ff676a36969418 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiLink, EuiIcon, EuiText, EuiSpacer } from '@elastic/eui'; + +import { useCollapsibleList } from './use_collapsible_list'; + +interface Props { + indices: string[] | string | undefined; +} + +export const CollapsibleIndicesList: React.FunctionComponent = ({ indices }) => { + const { hiddenItemsCount, isShowingFullList, items, setIsShowingFullList } = useCollapsibleList({ + items: indices, + }); + return items === 'all' ? ( + + ) : ( + <> + +
    + {items.map((index) => ( +
  • + + {index} + +
  • + ))} +
+
+ {hiddenItemsCount ? ( + <> + + + isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true) + } + > + {isShowingFullList ? ( + + ) : ( + + )}{' '} + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts new file mode 100644 index 00000000000000..d58edc983c5413 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/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 { CollapsibleIndicesList } from './collapsible_indices_list'; +export { CollapsibleDataStreamsList } from './collapsible_data_streams_list'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts new file mode 100644 index 00000000000000..bdeb801117de91 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { renderHook } from '@testing-library/react-hooks'; + +import { useCollapsibleList } from './use_collapsible_list'; + +describe('useCollapseList', () => { + it('handles undefined', () => { + const { result } = renderHook(() => useCollapsibleList({ items: undefined })); + expect(result.current.items).toBe('all'); + expect(result.current.hiddenItemsCount).toBe(0); + }); + + it('handles csv', () => { + const { result } = renderHook(() => useCollapsibleList({ items: 'a,b,c' })); + expect(result.current.items).toEqual(['a', 'b', 'c']); + expect(result.current.hiddenItemsCount).toBe(0); + }); + + it('hides items passed a defined maximum (10)', () => { + const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']; + const { result } = renderHook(() => useCollapsibleList({ items })); + expect(result.current.items).toEqual(items.slice(0, -1)); + expect(result.current.hiddenItemsCount).toBe(1); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts new file mode 100644 index 00000000000000..275915c5760afd --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.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 { useState } from 'react'; +import { csvToArray } from '../../../../common/lib'; + +type ChildItems = string[] | 'all'; + +interface Arg { + items: string[] | string | undefined; +} + +export interface ReturnValue { + items: ChildItems; + hiddenItemsCount: number; + isShowingFullList: boolean; + setIsShowingFullList: (showAll: boolean) => void; +} + +const maximumItemPreviewCount = 10; + +export const useCollapsibleList = ({ items }: Arg): ReturnValue => { + const [isShowingFullList, setIsShowingFullList] = useState(false); + const itemsArray = csvToArray(items); + const displayItems: ChildItems = + items === undefined + ? 'all' + : itemsArray.slice(0, isShowingFullList ? Infinity : maximumItemPreviewCount); + + const hiddenItemsCount = + itemsArray.length > maximumItemPreviewCount ? itemsArray.length - maximumItemPreviewCount : 0; + + return { + items: displayItems, + hiddenItemsCount, + setIsShowingFullList, + isShowingFullList, + }; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/data_stream_badge.tsx b/x-pack/plugins/snapshot_restore/public/application/components/data_stream_badge.tsx new file mode 100644 index 00000000000000..e7d3f59bd567a9 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/data_stream_badge.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiBadge } from '@elastic/eui'; + +export const DataStreamBadge: FunctionComponent = () => { + return ( + + {i18n.translate('xpack.snapshotRestore.policyForm.setSettings.dataStreamBadgeContent', { + defaultMessage: 'Data stream', + })} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/index.ts index f5bb8923898702..91266aae66e278 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/index.ts @@ -15,7 +15,7 @@ export { SnapshotDeleteProvider } from './snapshot_delete_provider'; export { RestoreSnapshotForm } from './restore_snapshot_form'; export { PolicyExecuteProvider } from './policy_execute_provider'; export { PolicyDeleteProvider } from './policy_delete_provider'; -export { CollapsibleIndicesList } from './collapsible_indices_list'; +export { CollapsibleIndicesList, CollapsibleDataStreamsList } from './collapsible_lists'; export { RetentionSettingsUpdateModalProvider, UpdateRetentionSettings, diff --git a/x-pack/plugins/snapshot_restore/public/application/components/lib/helpers.ts b/x-pack/plugins/snapshot_restore/public/application/components/lib/helpers.ts new file mode 100644 index 00000000000000..f21576778a0ea2 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/lib/helpers.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const orderDataStreamsAndIndices = ({ + dataStreams, + indices, +}: { + dataStreams: D[]; + indices: D[]; +}) => { + return dataStreams.concat(indices); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/lib/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/lib/index.ts new file mode 100644 index 00000000000000..a40695e9a20e87 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/lib/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 { orderDataStreamsAndIndices } from './helpers'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx index f9cad7cc4e0701..3e1fb9b6500b31 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx @@ -27,6 +27,7 @@ import { PolicyNavigation } from './navigation'; interface Props { policy: SlmPolicyPayload; + dataStreams: string[]; indices: string[]; currentUrl: string; isEditing?: boolean; @@ -39,6 +40,7 @@ interface Props { export const PolicyForm: React.FunctionComponent = ({ policy: originalPolicy, + dataStreams, indices, currentUrl, isEditing, @@ -71,6 +73,8 @@ export const PolicyForm: React.FunctionComponent = ({ }, }); + const isEditingManagedPolicy = Boolean(isEditing && policy.isManagedPolicy); + // Policy validation state const [validation, setValidation] = useState({ isValid: true, @@ -132,6 +136,7 @@ export const PolicyForm: React.FunctionComponent = ({ = ({ {currentStep === lastStep ? ( savePolicy()} isLoading={isSaving} diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts index 8b251de80a8e1e..a79a6ecb42e459 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts @@ -10,6 +10,7 @@ import { PolicyValidation } from '../../../services/validation'; export interface StepProps { policy: SlmPolicyPayload; indices: string[]; + dataStreams: string[]; updatePolicy: (updatedSettings: Partial, validationHelperData?: any) => void; isEditing: boolean; currentUrl: string; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx index b2422be3b78c38..6b253a3fada057 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx @@ -22,7 +22,7 @@ import { import { serializePolicy } from '../../../../../common/lib'; import { useServices } from '../../../app_context'; import { StepProps } from './'; -import { CollapsibleIndicesList } from '../../collapsible_indices_list'; +import { CollapsibleIndicesList } from '../../collapsible_lists'; export const PolicyStepReview: React.FunctionComponent = ({ policy, @@ -148,8 +148,8 @@ export const PolicyStepReview: React.FunctionComponent = ({ @@ -187,8 +187,8 @@ export const PolicyStepReview: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx deleted file mode 100644 index 07a62723123020..00000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx +++ /dev/null @@ -1,469 +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, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiDescribedFormGroup, - EuiTitle, - EuiFormRow, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - EuiSwitch, - EuiLink, - EuiSelectable, - EuiPanel, - EuiComboBox, - EuiToolTip, -} from '@elastic/eui'; -import { EuiSelectableOption } from '@elastic/eui'; -import { SlmPolicyPayload, SnapshotConfig } from '../../../../../common/types'; -import { documentationLinksService } from '../../../services/documentation'; -import { useServices } from '../../../app_context'; -import { StepProps } from './'; - -export const PolicyStepSettings: React.FunctionComponent = ({ - policy, - indices, - updatePolicy, - errors, -}) => { - const { i18n } = useServices(); - const { config = {}, isManagedPolicy } = policy; - - const updatePolicyConfig = (updatedFields: Partial): void => { - const newConfig = { ...config, ...updatedFields }; - updatePolicy({ - config: newConfig, - }); - }; - - // States for choosing all indices, or a subset, including caching previously chosen subset list - const [isAllIndices, setIsAllIndices] = useState(!Boolean(config.indices)); - const [indicesSelection, setIndicesSelection] = useState([...indices]); - const [indicesOptions, setIndicesOptions] = useState( - indices.map( - (index): EuiSelectableOption => ({ - label: index, - checked: - isAllIndices || - // If indices is a string, we default to custom input mode, so we mark individual indices - // as selected if user goes back to list mode - typeof config.indices === 'string' || - (Array.isArray(config.indices) && config.indices.includes(index)) - ? 'on' - : undefined, - }) - ) - ); - - // State for using selectable indices list or custom patterns - // Users with more than 100 indices will probably want to use an index pattern to select - // them instead, so we'll default to showing them the index pattern input. - const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>( - typeof config.indices === 'string' || - (Array.isArray(config.indices) && config.indices.length > 100) - ? 'custom' - : 'list' - ); - - // State for custom patterns - const [indexPatterns, setIndexPatterns] = useState( - typeof config.indices === 'string' ? config.indices.split(',') : [] - ); - - const renderIndicesField = () => { - const indicesSwitch = ( - - } - checked={isAllIndices} - disabled={isManagedPolicy} - data-test-subj="allIndicesToggle" - onChange={(e) => { - const isChecked = e.target.checked; - setIsAllIndices(isChecked); - if (isChecked) { - updatePolicyConfig({ indices: undefined }); - } else { - updatePolicyConfig({ - indices: - selectIndicesMode === 'custom' - ? indexPatterns.join(',') - : [...(indicesSelection || [])], - }); - } - }} - /> - ); - - return ( - -

- -

- - } - description={ - - } - fullWidth - > - - - {isManagedPolicy ? ( - - -

- } - > - {indicesSwitch} -
- ) : ( - indicesSwitch - )} - {isAllIndices ? null : ( - - - - - - - - { - setSelectIndicesMode('custom'); - updatePolicyConfig({ indices: indexPatterns.join(',') }); - }} - > - - - -
- ) : ( - - - - - - { - setSelectIndicesMode('list'); - updatePolicyConfig({ indices: indicesSelection }); - }} - > - - - - - ) - } - helpText={ - selectIndicesMode === 'list' ? ( - 0 ? ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = undefined; - }); - updatePolicyConfig({ indices: [] }); - setIndicesSelection([]); - }} - > - - - ) : ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = 'on'; - }); - updatePolicyConfig({ indices: [...indices] }); - setIndicesSelection([...indices]); - }} - > - - - ), - }} - /> - ) : null - } - isInvalid={Boolean(errors.indices)} - error={errors.indices} - > - {selectIndicesMode === 'list' ? ( - { - const newSelectedIndices: string[] = []; - options.forEach(({ label, checked }) => { - if (checked === 'on') { - newSelectedIndices.push(label); - } - }); - setIndicesOptions(options); - updatePolicyConfig({ indices: newSelectedIndices }); - setIndicesSelection(newSelectedIndices); - }} - searchable - height={300} - > - {(list, search) => ( - - {search} - {list} - - )} - - ) : ( - ({ label: index }))} - placeholder={i18n.translate( - 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder', - { - defaultMessage: 'Enter index patterns, i.e. logstash-*', - } - )} - selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))} - onCreateOption={(pattern: string) => { - if (!pattern.trim().length) { - return; - } - const newPatterns = [...indexPatterns, pattern]; - setIndexPatterns(newPatterns); - updatePolicyConfig({ - indices: newPatterns.join(','), - }); - }} - onChange={(patterns: Array<{ label: string }>) => { - const newPatterns = patterns.map(({ label }) => label); - setIndexPatterns(newPatterns); - updatePolicyConfig({ - indices: newPatterns.join(','), - }); - }} - /> - )} - - - )} - - - - ); - }; - - const renderIgnoreUnavailableField = () => ( - -

- -

- - } - description={ - - } - fullWidth - > - - - } - checked={Boolean(config.ignoreUnavailable)} - onChange={(e) => { - updatePolicyConfig({ - ignoreUnavailable: e.target.checked, - }); - }} - /> - -
- ); - - const renderPartialField = () => ( - -

- -

- - } - description={ - - } - fullWidth - > - - - } - checked={Boolean(config.partial)} - onChange={(e) => { - updatePolicyConfig({ - partial: e.target.checked, - }); - }} - /> - -
- ); - - const renderIncludeGlobalStateField = () => ( - -

- -

- - } - description={ - - } - fullWidth - > - - - } - checked={config.includeGlobalState === undefined || config.includeGlobalState} - onChange={(e) => { - updatePolicyConfig({ - includeGlobalState: e.target.checked, - }); - }} - /> - -
- ); - return ( -
- {/* Step title and doc link */} - - - -

- -

-
-
- - - - - - -
- - - {renderIndicesField()} - {renderIgnoreUnavailableField()} - {renderPartialField()} - {renderIncludeGlobalStateField()} -
- ); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts new file mode 100644 index 00000000000000..e0d632a58e4e18 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx new file mode 100644 index 00000000000000..3570c74fb8fd0d --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.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, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink } from '@elastic/eui'; + +interface Props { + onSelectionChange: (selection: 'all' | 'none') => void; + selectedIndicesAndDataStreams: string[]; + indices: string[]; + dataStreams: string[]; +} + +export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({ + onSelectionChange, + selectedIndicesAndDataStreams, + indices, + dataStreams, +}) => { + if (selectedIndicesAndDataStreams.length === 0) { + return ( + { + onSelectionChange('all'); + }} + > + + + ), + }} + /> + ); + } + + const indicesCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (indices.includes(v) ? acc + 1 : acc), + 0 + ); + const dataStreamsCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc), + 0 + ); + + return ( + { + onSelectionChange('none'); + }} + > + + + ), + }} + /> + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts new file mode 100644 index 00000000000000..9bf97af6400b52 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { determineListMode } from './helpers'; + +describe('helpers', () => { + describe('determineListMode', () => { + test('list length (> 100)', () => { + expect( + determineListMode({ + indices: Array.from(Array(101).keys()).map(String), + dataStreams: [], + configuredIndices: undefined, + }) + ).toBe('custom'); + + // The length of indices and data streams are cumulative + expect( + determineListMode({ + indices: Array.from(Array(51).keys()).map(String), + dataStreams: Array.from(Array(51).keys()).map(String), + configuredIndices: undefined, + }) + ).toBe('custom'); + + // Other values should result in list mode + expect( + determineListMode({ + indices: [], + dataStreams: [], + configuredIndices: undefined, + }) + ).toBe('list'); + }); + + test('configured indices is a string', () => { + expect( + determineListMode({ + indices: [], + dataStreams: [], + configuredIndices: 'test', + }) + ).toBe('custom'); + }); + + test('configured indices not included in current indices and data streams', () => { + expect( + determineListMode({ + indices: ['a'], + dataStreams: ['b'], + configuredIndices: ['a', 'b', 'c'], + }) + ).toBe('custom'); + }); + + test('configured indices included in current indices and data streams', () => { + expect( + determineListMode({ + indices: ['a'], + dataStreams: ['b'], + configuredIndices: ['a', 'b'], + }) + ).toBe('list'); + }); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx new file mode 100644 index 00000000000000..98ad2fe9c54894 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiSelectableOption } from '@elastic/eui'; +import { orderDataStreamsAndIndices } from '../../../../../lib'; +import { DataStreamBadge } from '../../../../../data_stream_badge'; + +export const mapSelectionToIndicesOptions = ({ + allSelected, + selection, + dataStreams, + indices, +}: { + allSelected: boolean; + selection: string[]; + dataStreams: string[]; + indices: string[]; +}): EuiSelectableOption[] => { + return orderDataStreamsAndIndices({ + dataStreams: dataStreams.map( + (dataStream): EuiSelectableOption => { + return { + label: dataStream, + append: , + checked: allSelected || selection.includes(dataStream) ? 'on' : undefined, + }; + } + ), + indices: indices.map( + (index): EuiSelectableOption => { + return { + label: index, + checked: allSelected || selection.includes(index) ? 'on' : undefined, + }; + } + ), + }); +}; + +/** + * @remark + * Users with more than 100 indices will probably want to use an index pattern to select + * them instead, so we'll default to showing them the index pattern input. Also show the custom + * list if we have no exact matches in the configured array to some existing index. + */ +export const determineListMode = ({ + configuredIndices, + indices, + dataStreams, +}: { + configuredIndices: string | string[] | undefined; + indices: string[]; + dataStreams: string[]; +}): 'custom' | 'list' => { + const indicesAndDataStreams = indices.concat(dataStreams); + return typeof configuredIndices === 'string' || + indicesAndDataStreams.length > 100 || + (Array.isArray(configuredIndices) && + // If not every past configured index maps to an existing index or data stream + // we also show the custom list + !configuredIndices.every((c) => indicesAndDataStreams.some((i) => i === c))) + ? 'custom' + : 'list'; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts new file mode 100644 index 00000000000000..e0d632a58e4e18 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx new file mode 100644 index 00000000000000..94854905e66863 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx @@ -0,0 +1,348 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiComboBox, + EuiDescribedFormGroup, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiPanel, + EuiSelectable, + EuiSelectableOption, + EuiSpacer, + EuiSwitch, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; + +import { SlmPolicyPayload } from '../../../../../../../../common/types'; +import { useServices } from '../../../../../../app_context'; +import { PolicyValidation } from '../../../../../../services/validation'; + +import { orderDataStreamsAndIndices } from '../../../../../lib'; +import { DataStreamBadge } from '../../../../../data_stream_badge'; + +import { mapSelectionToIndicesOptions, determineListMode } from './helpers'; + +import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; + +interface Props { + isManagedPolicy: boolean; + policy: SlmPolicyPayload; + indices: string[]; + dataStreams: string[]; + onUpdate: (arg: { indices?: string[] | string }) => void; + errors: PolicyValidation['errors']; +} + +/** + * In future we may be able to split data streams to its own field, but for now + * they share an array "indices" in the snapshot lifecycle policy config. See + * this github issue for progress: https://github.com/elastic/elasticsearch/issues/58474 + */ +export const IndicesAndDataStreamsField: FunctionComponent = ({ + isManagedPolicy, + dataStreams, + indices, + policy, + onUpdate, + errors, +}) => { + const { i18n } = useServices(); + const { config = {} } = policy; + + const indicesAndDataStreams = indices.concat(dataStreams); + + // We assume all indices if the config has no indices entry or if we receive an empty array + const [isAllIndices, setIsAllIndices] = useState( + !config.indices || (Array.isArray(config.indices) && config.indices.length === 0) + ); + + const [indicesAndDataStreamsSelection, setIndicesAndDataStreamsSelection] = useState( + () => + Array.isArray(config.indices) && !isAllIndices + ? indicesAndDataStreams.filter((i) => (config.indices! as string[]).includes(i)) + : [...indicesAndDataStreams] + ); + + // States for choosing all indices, or a subset, including caching previously chosen subset list + const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState< + EuiSelectableOption[] + >(() => + mapSelectionToIndicesOptions({ + selection: indicesAndDataStreamsSelection, + dataStreams, + indices, + allSelected: isAllIndices || typeof config.indices === 'string', + }) + ); + + // State for using selectable indices list or custom patterns + const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>(() => + determineListMode({ configuredIndices: config.indices, dataStreams, indices }) + ); + + // State for custom patterns + const [indexPatterns, setIndexPatterns] = useState(() => + typeof config.indices === 'string' + ? (config.indices as string).split(',') + : Array.isArray(config.indices) && config.indices + ? config.indices + : [] + ); + + const indicesSwitch = ( + + } + checked={isAllIndices} + disabled={isManagedPolicy} + data-test-subj="allIndicesToggle" + onChange={(e) => { + const isChecked = e.target.checked; + setIsAllIndices(isChecked); + if (isChecked) { + setIndicesAndDataStreamsSelection(indicesAndDataStreams); + setIndicesAndDataStreamsOptions( + mapSelectionToIndicesOptions({ + allSelected: isAllIndices || typeof config.indices === 'string', + dataStreams, + indices, + selection: indicesAndDataStreamsSelection, + }) + ); + onUpdate({ indices: undefined }); + } else { + onUpdate({ + indices: + selectIndicesMode === 'custom' + ? indexPatterns.join(',') + : [...(indicesAndDataStreamsSelection || [])], + }); + } + }} + /> + ); + + return ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + {isManagedPolicy ? ( + + +

+ } + > + {indicesSwitch} +
+ ) : ( + indicesSwitch + )} + {isAllIndices ? null : ( + + + + + + + + { + setSelectIndicesMode('custom'); + onUpdate({ indices: indexPatterns.join(',') }); + }} + > + + + +
+ ) : ( + + + + + + { + setSelectIndicesMode('list'); + onUpdate({ indices: indicesAndDataStreamsSelection }); + }} + > + + + + + ) + } + helpText={ + selectIndicesMode === 'list' ? ( + { + if (selection === 'all') { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = 'on'; + }); + onUpdate({ indices: [...indicesAndDataStreams] }); + setIndicesAndDataStreamsSelection([...indicesAndDataStreams]); + } else { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = undefined; + }); + onUpdate({ indices: [] }); + setIndicesAndDataStreamsSelection([]); + } + }} + selectedIndicesAndDataStreams={indicesAndDataStreamsSelection} + indices={indices} + dataStreams={dataStreams} + /> + ) : null + } + isInvalid={Boolean(errors.indices)} + error={errors.indices} + > + {selectIndicesMode === 'list' ? ( + { + const newSelectedIndices: string[] = []; + options.forEach(({ label, checked }) => { + if (checked === 'on') { + newSelectedIndices.push(label); + } + }); + setIndicesAndDataStreamsOptions(options); + onUpdate({ indices: newSelectedIndices }); + setIndicesAndDataStreamsSelection(newSelectedIndices); + }} + searchable + height={300} + > + {(list, search) => ( + + {search} + {list} + + )} + + ) : ( + ({ + label: index, + value: { isDataStream: false }, + })), + dataStreams: dataStreams.map((dataStream) => ({ + label: dataStream, + value: { isDataStream: true }, + })), + })} + renderOption={({ label, value }) => { + if (value?.isDataStream) { + return ( + + {label} + + + + + ); + } + return label; + }} + placeholder={i18n.translate( + 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder', + { + defaultMessage: 'Enter index patterns, i.e. logstash-*', + } + )} + selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))} + onCreateOption={(pattern: string) => { + if (!pattern.trim().length) { + return; + } + const newPatterns = [...indexPatterns, pattern]; + setIndexPatterns(newPatterns); + onUpdate({ + indices: newPatterns.join(','), + }); + }} + onChange={(patterns: Array<{ label: string }>) => { + const newPatterns = patterns.map(({ label }) => label); + setIndexPatterns(newPatterns); + onUpdate({ + indices: newPatterns.join(','), + }); + }} + /> + )} + + + )} + + + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts new file mode 100644 index 00000000000000..24e9b36e748899 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/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 { PolicyStepSettings } from './step_settings'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx new file mode 100644 index 00000000000000..9d43c45d17ea7a --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescribedFormGroup, + EuiTitle, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; + +import { SlmPolicyPayload } from '../../../../../../common/types'; +import { documentationLinksService } from '../../../../services/documentation'; +import { StepProps } from '../'; + +import { IndicesAndDataStreamsField } from './fields'; + +export const PolicyStepSettings: React.FunctionComponent = ({ + policy, + indices, + dataStreams, + updatePolicy, + errors, +}) => { + const { config = {}, isManagedPolicy } = policy; + + const updatePolicyConfig = (updatedFields: Partial): void => { + const newConfig = { ...config, ...updatedFields }; + updatePolicy({ + config: newConfig, + }); + }; + + const renderIgnoreUnavailableField = () => ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={Boolean(config.ignoreUnavailable)} + onChange={(e) => { + updatePolicyConfig({ + ignoreUnavailable: e.target.checked, + }); + }} + /> + +
+ ); + + const renderPartialField = () => ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={Boolean(config.partial)} + onChange={(e) => { + updatePolicyConfig({ + partial: e.target.checked, + }); + }} + /> + +
+ ); + + const renderIncludeGlobalStateField = () => ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={config.includeGlobalState === undefined || config.includeGlobalState} + onChange={(e) => { + updatePolicyConfig({ + includeGlobalState: e.target.checked, + }); + }} + /> + +
+ ); + return ( +
+ {/* Step title and doc link */} + + + +

+ +

+
+
+ + + + + + +
+ + + + + {renderIgnoreUnavailableField()} + {renderPartialField()} + {renderIncludeGlobalStateField()} +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts index 3f3db0ff28eca9..182d4ef8f583a2 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts @@ -14,6 +14,6 @@ export interface StepProps { updateCurrentStep: (step: number) => void; } -export { RestoreSnapshotStepLogistics } from './step_logistics'; +export { RestoreSnapshotStepLogistics } from './step_logistics/step_logistics'; export { RestoreSnapshotStepSettings } from './step_settings'; export { RestoreSnapshotStepReview } from './step_review'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx new file mode 100644 index 00000000000000..877dbe89639260 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.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, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink } from '@elastic/eui'; + +interface Props { + onSelectionChange: (selection: 'all' | 'none') => void; + selectedIndicesAndDataStreams: string[]; + indices: string[]; + dataStreams: string[]; +} + +export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({ + onSelectionChange, + selectedIndicesAndDataStreams, + indices, + dataStreams, +}) => { + if (selectedIndicesAndDataStreams.length === 0) { + return ( + { + onSelectionChange('all'); + }} + > + + + ), + }} + /> + ); + } + + const indicesCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (indices.includes(v) ? acc + 1 : acc), + 0 + ); + const dataStreamsCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc), + 0 + ); + + return ( + { + onSelectionChange('none'); + }} + > + + + ), + }} + /> + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx new file mode 100644 index 00000000000000..64fce4dcfac431 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +import { documentationLinksService } from '../../../../services/documentation'; + +const i18nTexts = { + callout: { + title: (count: number) => + i18n.translate('xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.title', { + defaultMessage: + 'This snapshot contains {count, plural, one {a data stream} other {data streams}}', + values: { count }, + }), + body: () => ( + + {i18n.translate( + 'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink', + { defaultMessage: 'Learn more' } + )} + + ), + }} + /> + ), + }, +}; + +interface Props { + dataStreamsCount: number; +} + +export const DataStreamsGlobalStateCallOut: FunctionComponent = ({ dataStreamsCount }) => { + return ( + + {i18nTexts.callout.body()} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts new file mode 100644 index 00000000000000..8f4efcf2a91f1a --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/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 { RestoreSnapshotStepLogistics } from './step_logistics'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx similarity index 69% rename from x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx rename to x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index c80c5a2e4c01d7..d9fd4cca0d614f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -21,10 +21,22 @@ import { EuiComboBox, } from '@elastic/eui'; import { EuiSelectableOption } from '@elastic/eui'; -import { RestoreSettings } from '../../../../../common/types'; -import { documentationLinksService } from '../../../services/documentation'; -import { useServices } from '../../../app_context'; -import { StepProps } from './'; + +import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib'; +import { RestoreSettings } from '../../../../../../common/types'; + +import { documentationLinksService } from '../../../../services/documentation'; + +import { useServices } from '../../../../app_context'; + +import { orderDataStreamsAndIndices } from '../../../lib'; +import { DataStreamBadge } from '../../../data_stream_badge'; + +import { StepProps } from '../index'; + +import { DataStreamsGlobalStateCallOut } from './data_streams_global_state_call_out'; + +import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; export const RestoreSnapshotStepLogistics: React.FunctionComponent = ({ snapshotDetails, @@ -34,10 +46,30 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = }) => { const { i18n } = useServices(); const { - indices: snapshotIndices, + indices: unfilteredSnapshotIndices, + dataStreams: snapshotDataStreams = [], includeGlobalState: snapshotIncludeGlobalState, } = snapshotDetails; + const snapshotIndices = unfilteredSnapshotIndices.filter( + (index) => !isDataStreamBackingIndex(index) + ); + const snapshotIndicesAndDataStreams = snapshotIndices.concat(snapshotDataStreams); + + const comboBoxOptions = orderDataStreamsAndIndices<{ + label: string; + value: { isDataStream: boolean; name: string }; + }>({ + dataStreams: snapshotDataStreams.map((dataStream) => ({ + label: dataStream, + value: { isDataStream: true, name: dataStream }, + })), + indices: snapshotIndices.map((index) => ({ + label: index, + value: { isDataStream: false, name: index }, + })), + }); + const { indices: restoreIndices, renamePattern, @@ -47,28 +79,50 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } = restoreSettings; // States for choosing all indices, or a subset, including caching previously chosen subset list - const [isAllIndices, setIsAllIndices] = useState(!Boolean(restoreIndices)); - const [indicesOptions, setIndicesOptions] = useState( - snapshotIndices.map( - (index): EuiSelectableOption => ({ - label: index, - checked: - isAllIndices || - // If indices is a string, we default to custom input mode, so we mark individual indices - // as selected if user goes back to list mode - typeof restoreIndices === 'string' || - (Array.isArray(restoreIndices) && restoreIndices.includes(index)) - ? 'on' - : undefined, - }) - ) + const [isAllIndicesAndDataStreams, setIsAllIndicesAndDataStreams] = useState( + !Boolean(restoreIndices) + ); + const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState< + EuiSelectableOption[] + >(() => + orderDataStreamsAndIndices({ + dataStreams: snapshotDataStreams.map( + (dataStream): EuiSelectableOption => ({ + label: dataStream, + append: , + checked: + isAllIndicesAndDataStreams || + // If indices is a string, we default to custom input mode, so we mark individual indices + // as selected if user goes back to list mode + typeof restoreIndices === 'string' || + (Array.isArray(restoreIndices) && restoreIndices.includes(dataStream)) + ? 'on' + : undefined, + }) + ), + indices: snapshotIndices.map( + (index): EuiSelectableOption => ({ + label: index, + checked: + isAllIndicesAndDataStreams || + // If indices is a string, we default to custom input mode, so we mark individual indices + // as selected if user goes back to list mode + typeof restoreIndices === 'string' || + (Array.isArray(restoreIndices) && restoreIndices.includes(index)) + ? 'on' + : undefined, + }) + ), + }) ); // State for using selectable indices list or custom patterns // Users with more than 100 indices will probably want to use an index pattern to select // them instead, so we'll default to showing them the index pattern input. const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>( - typeof restoreIndices === 'string' || snapshotIndices.length > 100 ? 'custom' : 'list' + typeof restoreIndices === 'string' || snapshotIndicesAndDataStreams.length > 100 + ? 'custom' + : 'list' ); // State for custom patterns @@ -83,13 +137,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = // Caching state for togglable settings const [cachedRestoreSettings, setCachedRestoreSettings] = useState({ - indices: [...snapshotIndices], + indices: [...snapshotIndicesAndDataStreams], renamePattern: '', renameReplacement: '', }); return ( -
+
{/* Step title and doc link */} @@ -118,6 +175,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = + + {snapshotDataStreams.length ? ( + <> + + + + ) : undefined} + {/* Indices */} @@ -126,16 +191,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =

} description={ } @@ -146,14 +211,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } - checked={isAllIndices} + checked={isAllIndicesAndDataStreams} onChange={(e) => { const isChecked = e.target.checked; - setIsAllIndices(isChecked); + setIsAllIndicesAndDataStreams(isChecked); if (isChecked) { updateRestoreSettings({ indices: undefined }); } else { @@ -166,7 +231,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } }} /> - {isAllIndices ? null : ( + {isAllIndicesAndDataStreams ? null : ( = @@ -210,8 +275,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = }} > @@ -220,52 +285,35 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } helpText={ selectIndicesMode === 'list' ? ( - 0 ? ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = undefined; - }); - updateRestoreSettings({ indices: [] }); - setCachedRestoreSettings({ - ...cachedRestoreSettings, - indices: [], - }); - }} - > - - - ) : ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = 'on'; - }); - updateRestoreSettings({ indices: [...snapshotIndices] }); - setCachedRestoreSettings({ - ...cachedRestoreSettings, - indices: [...snapshotIndices], - }); - }} - > - - - ), + { + if (selection === 'all') { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = 'on'; + }); + updateRestoreSettings({ + indices: [...snapshotIndicesAndDataStreams], + }); + setCachedRestoreSettings({ + ...cachedRestoreSettings, + indices: [...snapshotIndicesAndDataStreams], + }); + } else { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = undefined; + }); + updateRestoreSettings({ indices: [] }); + setCachedRestoreSettings({ + ...cachedRestoreSettings, + indices: [], + }); + } }} + selectedIndicesAndDataStreams={csvToArray(restoreIndices)} + indices={snapshotIndices} + dataStreams={snapshotDataStreams} /> ) : null } @@ -275,7 +323,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = {selectIndicesMode === 'list' ? ( { const newSelectedIndices: string[] = []; options.forEach(({ label, checked }) => { @@ -283,7 +331,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = newSelectedIndices.push(label); } }); - setIndicesOptions(options); + setIndicesAndDataStreamsOptions(options); updateRestoreSettings({ indices: [...newSelectedIndices] }); setCachedRestoreSettings({ ...cachedRestoreSettings, @@ -302,7 +350,24 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = ) : ( ({ label: index }))} + options={comboBoxOptions} + renderOption={({ value }) => { + return value?.isDataStream ? ( + + {value.name} + + + + + ) : ( + value?.name + ); + }} placeholder={i18n.translate( 'xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder', { @@ -336,22 +401,22 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = - {/* Rename indices */} + {/* Rename data streams and indices */}

} description={ } fullWidth @@ -361,8 +426,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } checked={isRenamingIndices} @@ -405,7 +470,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = > { setCachedRestoreSettings({ ...cachedRestoreSettings, @@ -431,7 +496,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = > { setCachedRestoreSettings({ ...cachedRestoreSettings, diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx index 27a3717566d93f..5dacba506fe188 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx @@ -24,7 +24,7 @@ import { import { serializeRestoreSettings } from '../../../../../common/lib'; import { useServices } from '../../../app_context'; import { StepProps } from './'; -import { CollapsibleIndicesList } from '../../collapsible_indices_list'; +import { CollapsibleIndicesList } from '../../collapsible_lists/collapsible_indices_list'; export const RestoreSnapshotStepReview: React.FunctionComponent = ({ restoreSettings, @@ -73,8 +73,8 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx index 5f3ebf804c5e12..b9a2d7e4b7cd97 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx @@ -18,6 +18,7 @@ import { EuiSwitch, EuiTitle, EuiLink, + EuiCallOut, } from '@elastic/eui'; import { RestoreSettings } from '../../../../../common/types'; import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants'; @@ -28,10 +29,12 @@ import { StepProps } from './'; export const RestoreSnapshotStepSettings: React.FunctionComponent = ({ restoreSettings, updateRestoreSettings, + snapshotDetails, errors, }) => { const { i18n } = useServices(); const { indexSettings, ignoreIndexSettings } = restoreSettings; + const { dataStreams } = snapshotDetails; // State for index setting toggles const [isUsingIndexSettings, setIsUsingIndexSettings] = useState(Boolean(indexSettings)); @@ -96,6 +99,23 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( + {dataStreams?.length ? ( + <> + + + + + + ) : undefined} {/* Modify index settings */} diff --git a/x-pack/plugins/snapshot_restore/public/application/index.scss b/x-pack/plugins/snapshot_restore/public/application/index.scss index b680f4d3ebf903..3e16e3b5301e79 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/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx index 7bcee4f5f6621d..e69b0fad8014ee 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx @@ -236,8 +236,8 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { 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 739c72fe03a6fa..3b18af7cebbf3b 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/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx index 287a77493307da..1a0c26c8544907 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx @@ -22,6 +22,7 @@ import { DataPlaceholder, FormattedDateTime, CollapsibleIndicesList, + CollapsibleDataStreamsList, } from '../../../../../components'; import { linkToPolicy } from '../../../../../services/navigation'; import { SnapshotState } from './snapshot_state'; @@ -40,6 +41,7 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { // TODO: Add a tooltip explaining that: a false value means that the cluster global state // is not stored as part of the snapshot. includeGlobalState, + dataStreams, indices, state, startTimeInMillis, @@ -135,6 +137,22 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { + + + + + + + + + + + + diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx index 6d1a432be7f9f0..90cd26c821c5e2 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx @@ -25,13 +25,8 @@ export const PolicyAdd: React.FunctionComponent = ({ const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); - const { - error: errorLoadingIndices, - isLoading: isLoadingIndices, - data: { indices } = { - indices: [], - }, - } = useLoadIndices(); + const { error: errorLoadingIndices, isLoading: isLoadingIndices, data } = useLoadIndices(); + const { indices, dataStreams } = data ?? { indices: [], dataStreams: [] }; // Set breadcrumb and page title useEffect(() => { @@ -123,6 +118,7 @@ export const PolicyAdd: React.FunctionComponent = ({ { }; export const useLoadIndices = () => { - return useRequest({ + return useRequest({ path: `${API_BASE_PATH}policies/indices`, method: 'get', }); diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts index 27a565ccb74bc9..b4d0493098bbc5 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts @@ -18,6 +18,6 @@ export const sendRequest = (config: SendRequestConfig) => { return _sendRequest(httpService.httpClient, config); }; -export const useRequest = (config: UseRequestConfig) => { - return _useRequest(httpService.httpClient, config); +export const useRequest = (config: UseRequestConfig) => { + return _useRequest(httpService.httpClient, config); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts index 0720994ca76693..24960b2533230e 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts @@ -48,6 +48,7 @@ export const validatePolicy = ( snapshotName: [], schedule: [], repository: [], + dataStreams: [], indices: [], expireAfterValue: [], minCount: [], @@ -106,7 +107,7 @@ export const validatePolicy = ( if (config && Array.isArray(config.indices) && config.indices.length === 0) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredErrorMessage', { - defaultMessage: 'You must select at least one index.', + defaultMessage: 'You must select at least one data stream or index.', }) ); } diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts index 5c1a1fbfab12d1..93e278e51f0939 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts @@ -48,7 +48,7 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida if (Array.isArray(indices) && indices.length === 0) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.restoreValidation.indicesRequiredError', { - defaultMessage: 'You must select at least one index.', + defaultMessage: 'You must select at least one data stream or index.', }) ); } @@ -93,7 +93,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida 'xpack.snapshotRestore.restoreValidation.indexSettingsNotModifiableError', { defaultMessage: 'You can’t modify: {settings}', - // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299 values: { settings: unmodifiableSettings.map((setting: string, index: number) => index === 0 ? `${setting} ` : setting @@ -131,7 +130,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida validation.errors.ignoreIndexSettings.push( i18n.translate('xpack.snapshotRestore.restoreValidation.indexSettingsNotRemovableError', { defaultMessage: 'You can’t reset: {settings}', - // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299 values: { settings: unremovableSettings.map((setting: string, index: number) => index === 0 ? `${setting} ` : setting diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts index eb29b7bad37e66..b96d305fa4a874 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -6,6 +6,7 @@ import { addBasePath } from '../helpers'; import { registerPolicyRoutes } from './policy'; import { RouterMock, routeDependencies, RequestMock } from '../../test/helpers'; +import { ResolveIndexResponseFromES } from '../../types'; describe('[Snapshot and Restore API Routes] Policy', () => { const mockEsPolicy = { @@ -324,27 +325,45 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }; it('should arrify and sort index names returned from ES', async () => { - const mockEsResponse = [ - { - index: 'fooIndex', - }, - { - index: 'barIndex', - }, - ]; + const mockEsResponse: ResolveIndexResponseFromES = { + indices: [ + { + name: 'fooIndex', + attributes: ['open'], + }, + { + name: 'barIndex', + attributes: ['open'], + data_stream: 'testDataStream', + }, + ], + aliases: [], + data_streams: [ + { + name: 'testDataStream', + backing_indices: ['barIndex'], + timestamp_field: '@timestamp', + }, + ], + }; router.callAsCurrentUserResponses = [mockEsResponse]; const expectedResponse = { - indices: ['barIndex', 'fooIndex'], + indices: ['fooIndex'], + dataStreams: ['testDataStream'], }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should return empty array if no indices returned from ES', async () => { - const mockEsResponse: any[] = []; + const mockEsResponse: ResolveIndexResponseFromES = { + indices: [], + aliases: [], + data_streams: [], + }; router.callAsCurrentUserResponses = [mockEsResponse]; - const expectedResponse = { indices: [] }; + const expectedResponse = { indices: [], dataStreams: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index 90667eda23b351..b8e70125295544 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -5,10 +5,10 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { SlmPolicyEs } from '../../../common/types'; +import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types'; import { deserializePolicy, serializePolicy } from '../../../common/lib'; import { getManagedPolicyNames } from '../../lib'; -import { RouteDependencies } from '../../types'; +import { RouteDependencies, ResolveIndexResponseFromES } from '../../types'; import { addBasePath } from '../helpers'; import { nameParameterSchema, policySchema } from './validate_schemas'; @@ -232,17 +232,26 @@ export function registerPolicyRoutes({ const { callAsCurrentUser } = ctx.snapshotRestore!.client; try { - const indices: Array<{ - index: string; - }> = await callAsCurrentUser('cat.indices', { - format: 'json', - h: 'index', - }); + const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser( + 'transport.request', + { + method: 'GET', + path: `_resolve/index/*`, + query: { + expand_wildcards: 'all,hidden', + }, + } + ); + + const body: PolicyIndicesResponse = { + dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(), + indices: resolvedIndicesResponse.indices + .flatMap((index) => (index.data_stream ? [] : index.name)) + .sort(), + }; return res.ok({ - body: { - indices: indices.map(({ index }) => index).sort(), - }, + body, }); } catch (e) { if (isEsError(e)) { diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index f913299fc39925..a7e61d1e7c02a9 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -15,6 +15,7 @@ const defaultSnapshot = { versionId: undefined, version: undefined, indices: [], + dataStreams: [], includeGlobalState: undefined, state: undefined, startTime: undefined, diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index 7794156eb1b888..8cfcaec1a2cd1b 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -31,4 +31,20 @@ export interface RouteDependencies { }; } +/** + * An object representing a resolved index, data stream or alias + */ +interface IndexAndAliasFromEs { + name: string; + // per https://github.com/elastic/elasticsearch/pull/57626 + attributes: Array<'open' | 'closed' | 'hidden' | 'frozen'>; + data_stream?: string; +} + +export interface ResolveIndexResponseFromES { + indices: IndexAndAliasFromEs[]; + aliases: IndexAndAliasFromEs[]; + data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>; +} + export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts index d6a55579b322d4..e59f4689d9e3f6 100644 --- a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts @@ -13,13 +13,23 @@ export const getSnapshot = ({ state = 'SUCCESS', indexFailures = [], totalIndices = getRandomNumber(), -} = {}) => ({ + totalDataStreams = getRandomNumber(), +}: Partial<{ + repository: string; + snapshot: string; + uuid: string; + state: string; + indexFailures: any[]; + totalIndices: number; + totalDataStreams: number; +}> = {}) => ({ repository, snapshot, uuid, versionId: 8000099, version: '8.0.0', indices: new Array(totalIndices).fill('').map(getRandomString), + dataStreams: new Array(totalDataStreams).fill('').map(getRandomString), includeGlobalState: 1, state, startTime: '2019-05-23T06:25:15.896Z', 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 4c9f62503a21bc..87c2fee4ea9bf1 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 eac9d09685a425..07a9076359f0c9 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 17292adad3eb4a..92374908c60f75 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 7a9fa0c45e15f6..4c690a5675f61b 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 fec72c317225b5..771b4e2d7d9cb6 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 c63f4ac72ed207..4a691e17011e8d 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/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap index ed82dc65eb4108..b9bb206b8056f4 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -1,158 +1,158 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Telemetry Collection: Get Aggregated Stats OSS-like telemetry (no license nor X-Pack telemetry) 1`] = ` -Array [ - Object { - "cluster_name": "test", - "cluster_stats": Object { - "nodes": Object { - "usage": Object { - "nodes": Array [ - Object { - "aggregations": Object { - "terms": Object { - "bytes": 2, - }, - }, - "node_id": "some_node_id", - "rest_actions": Object { - "nodes_usage_action": 1, +Object { + "cluster_name": "test", + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, }, - "since": 1588616945163, - "timestamp": 1588617023177, }, - ], - }, - }, - }, - "cluster_uuid": "test", - "collection": "local", - "stack_stats": Object { - "kibana": Object { - "count": 1, - "great": "googlymoogly", - "indices": 1, - "os": Object { - "platformReleases": Array [ - Object { - "count": 1, - "platformRelease": "iv", - }, - ], - "platforms": Array [ - Object { - "count": 1, - "platform": "rocky", + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, }, - ], - }, - "plugins": Object { - "clouds": Object { - "chances": 95, + "since": 1588616945163, + "timestamp": 1588617023177, }, - "localization": Object { - "integrities": Object {}, - "labelsCount": 0, - "locale": "en", - }, - "rain": Object { - "chances": 2, - }, - "snow": Object { - "chances": 0, - }, - "sun": Object { - "chances": 5, + ], + }, + }, + }, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "data": Array [], + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", }, - }, - "versions": Array [ + ], + "platforms": Array [ Object { "count": 1, - "version": "8675309", + "platform": "rocky", }, ], }, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], }, - "version": "8.0.0", }, -] + "timestamp": Any, + "version": "8.0.0", +} `; exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry (license + X-Pack) 1`] = ` -Array [ - Object { - "cluster_name": "test", - "cluster_stats": Object { - "nodes": Object { - "usage": Object { - "nodes": Array [ - Object { - "aggregations": Object { - "terms": Object { - "bytes": 2, - }, - }, - "node_id": "some_node_id", - "rest_actions": Object { - "nodes_usage_action": 1, +Object { + "cluster_name": "test", + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, }, - "since": 1588616945163, - "timestamp": 1588617023177, }, - ], - }, - }, - }, - "cluster_uuid": "test", - "collection": "local", - "stack_stats": Object { - "kibana": Object { - "count": 1, - "great": "googlymoogly", - "indices": 1, - "os": Object { - "platformReleases": Array [ - Object { - "count": 1, - "platformRelease": "iv", - }, - ], - "platforms": Array [ - Object { - "count": 1, - "platform": "rocky", + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, }, - ], - }, - "plugins": Object { - "clouds": Object { - "chances": 95, + "since": 1588616945163, + "timestamp": 1588617023177, }, - "localization": Object { - "integrities": Object {}, - "labelsCount": 0, - "locale": "en", - }, - "rain": Object { - "chances": 2, - }, - "snow": Object { - "chances": 0, - }, - "sun": Object { - "chances": 5, + ], + }, + }, + }, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "data": Array [], + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", }, - }, - "versions": Array [ + ], + "platforms": Array [ Object { "count": 1, - "version": "8675309", + "platform": "rocky", }, ], }, - "xpack": Object {}, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], }, - "version": "8.0.0", + "xpack": Object {}, }, -] + "timestamp": Any, + "version": "8.0.0", +} `; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index a8311933f05317..24382fb89d3373 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -85,7 +85,11 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { } as any, context ); - expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + stats.forEach((entry) => { + expect(entry).toMatchSnapshot({ + timestamp: expect.any(String), + }); + }); }); test('X-Pack telemetry (license + X-Pack)', async () => { @@ -123,6 +127,10 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { } as any, context ); - expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + stats.forEach((entry) => { + expect(entry).toMatchSnapshot({ + timestamp: expect.any(String), + }); + }); }); }); diff --git a/x-pack/plugins/transform/public/app/index.scss b/x-pack/plugins/transform/public/app/index.scss index beb5ee6be67e66..cc5cc87c754c9c 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 074e93e10fd12b..5c5d270d324ffc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9214,9 +9214,6 @@ "xpack.maps.source.kbnTMSDescription": "kibana.yml で構成されたマップタイルです", "xpack.maps.source.kbnTMSTitle": "カスタムタイルマップサービス", "xpack.maps.source.mapSettingsPanel.initialLocationLabel": "マップの初期位置情報", - "xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "レイヤー名", - "xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大ズーム", - "xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小ズーム", "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "ベトルタイルレイヤー", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "ズームレベル", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "レイヤー名", @@ -9793,8 +9790,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} 影響因子が見つかりません", @@ -13390,7 +13385,6 @@ "xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "いいえ", "xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "グローバルステータスを含める", "xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "はい", - "xpack.snapshotRestore.policyDetails.indicesLabel": "インデックス", "xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "「{snapshotName}」が進行中", "xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日付", "xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "ポリシー「{name}」の前回のエラーの詳細", @@ -13498,10 +13492,8 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "いいえ", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバルステータスを含める", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "はい", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "インデックス", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "ポリシー名", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "いいえ", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "部分シャードを許可", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "はい", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "レポジトリ", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "スケジュール", @@ -13510,7 +13502,6 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "スナップショット名", "xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "まとめ", "xpack.snapshotRestore.policyForm.stepReviewTitle": "レビューポリシー", - "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "システムインデックスを含むすべてのインデックス", "xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "すべて選択解除", "xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "スナップショット設定ドキュメント", "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "スナップショットの撮影時に利用不可能なインデックスを無視します。これが設定されていない場合、スナップショット全体がエラーになります。", @@ -13518,19 +13509,15 @@ "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "利用不可能なインデックスを無視", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "スナップショットの一部としてクラスターのグローバルステータスを格納します。", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "グローバルステータスを含める", - "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "バックアップするインデックスです。", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "インデックスパターン", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力", - "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "インデックス", "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "インデックスパターンを使用", - "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "インデックスを選択", "xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "クラウドで管理されたポリシーにはすべてのインデックスが必要です。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "利用不可能なプライマリシャードのインデックスのスナップショットを許可します。これが設定されていない場合、スナップショット全体がエラーになります。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "部分インデックスを許可", "xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "部分インデックスを許可", "xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "グローバルステータスを含める", "xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "すべて選択", - "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}がバックアップされます。{selectOrDeselectAllLink}", "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "インデックスを選択", "xpack.snapshotRestore.policyForm.stepSettingsTitle": "スナップショット設定", "xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "スナップショットライフサイクルポリシーを管理するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です: {missingPrivileges}。", @@ -13877,31 +13864,22 @@ "xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "インデックス設定", "xpack.snapshotRestore.restoreForm.nextButtonLabel": "次へ", "xpack.snapshotRestore.restoreForm.savingButtonLabel": "復元中...", - "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "システムインデックスを含むすべてのインデックス", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "すべて選択解除", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "スナップショットと復元ドキュメント", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "現在クラスターに存在しないテンプレートを復元し、テンプレートを同じ名前で上書きします。永続的な設定も復元します。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "このスナップショットでは使用できません。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "グローバル状態の復元", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "存在しない場合は、新しいインデックスを作成します。閉じていて、スナップショットインデックスと同じ数のシャードがある場合は、既存のインデックスを復元します。", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "インデックスパターン", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "インデックス", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "インデックスパターンを使用", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "インデックスを選択", "xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "すべてのシャードのスナップショットがないインデックスを復元できます。", "xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分復元", "xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分復元", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "復元時にインデックス名を変更します。", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "インデックス名の変更", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "インデックス名の変更", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "正規表現を使用", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "取り込みパターン", "xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "置換パターン", "xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "すべて選択", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}が復元されます。{selectOrDeselectAllLink}", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "インデックスを選択", "xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "詳細を復元", "xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "実行する設定を復元", "xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON", @@ -13911,7 +13889,6 @@ "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "はい", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修正", - "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "インデックス", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "インデックス設定の修正はありません", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "いいえ", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分復元", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9d0bd95526670c..c71215d2bfb740 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9218,9 +9218,6 @@ "xpack.maps.source.kbnTMSDescription": "在 kibana.yml 中配置的地图磁贴", "xpack.maps.source.kbnTMSTitle": "定制磁贴地图服务", "xpack.maps.source.mapSettingsPanel.initialLocationLabel": "初始地图位置", - "xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "图层名称", - "xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大缩放", - "xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小缩放", "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "矢量磁贴图层", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "缩放级别", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "图层名称", @@ -9797,8 +9794,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} 影响因素", @@ -13395,7 +13390,6 @@ "xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "否", "xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "包括全局状态", "xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "是", - "xpack.snapshotRestore.policyDetails.indicesLabel": "索引", "xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "“{snapshotName}”正在进行中", "xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日期", "xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "策略“{name}”的上次失败详情", @@ -13503,10 +13497,8 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "否", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "包括全局状态", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "是", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "索引", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "策略名称", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "否", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "允许部分分片", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "是", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "存储库", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "计划", @@ -13515,7 +13507,6 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "快照名称", "xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "总结", "xpack.snapshotRestore.policyForm.stepReviewTitle": "复查策略", - "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "所有索引,包括系统索引", "xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "取消全选", "xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "快照设置文档", "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "拍取快照时忽略不可用的索引。否则,整个快照将失败。", @@ -13523,19 +13514,15 @@ "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "忽略不可用索引", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "将集群的全局状态存储为快照的一部分。", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "包括全局状态", - "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "要备份的索引。", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "索引模式", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*", - "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "索引", "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "使用索引模式", - "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "选择索引", "xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "云托管的策略需要所有索引。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "允许具有不可用主分片的索引的快照。否则,整个快照将失败。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "允许部分索引", "xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "允许部分索引", "xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "包括全局状态", "xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "全选", - "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "将备份 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}", "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "选择索引", "xpack.snapshotRestore.policyForm.stepSettingsTitle": "快照设置", "xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "要管理快照生命周期策略,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。", @@ -13882,31 +13869,22 @@ "xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "索引设置", "xpack.snapshotRestore.restoreForm.nextButtonLabel": "下一步", "xpack.snapshotRestore.restoreForm.savingButtonLabel": "正在还原……", - "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "所有索引,包括系统索引", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "取消全选", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "快照和还原文档", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "还原当前在集群中不存在的模板并覆盖同名模板。同时还原永久性设置。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "不适用于此快照。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "还原全局状态", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "如果不存在,则创建新索引。如果现有索引已关闭且与快照索引有相同数目的分片,则还原现有索引。", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "索引模式", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "索引", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "使用索引模式", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "选择索引", "xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "允许还原不具有所有分片的快照的索引。", "xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分还原", "xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分还原", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "还原时重命名索引。", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "重命名索引", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "重命名索引", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "使用正则表达式", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "捕获模式", "xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "替换模式", "xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "全选", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "将还原 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "选择索引", "xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "还原详情", "xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "还原要执行的设置", "xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON", @@ -13916,7 +13894,6 @@ "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "鏄", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修改", - "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "索引", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "无索引设置修改", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "否", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分还原", 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 957c79a5c51239..655f64995d147e 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 053541d84c4313..08616b2895a804 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 58a9685fc73d9d..39c59a10fbc813 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 1f8bfde2cd22b4..4a6c13bf7f1a6c 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 0b095cdc269847..fd6a3d64bd4be5 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 94bea3c51760d5..1b26b1157add9e 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 f0b131deb14904..c8ad5f5b7080e8 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 cb905023cae449..1849a7ec9817ad 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 c19aec2a993db5..b79fa0ea940506 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 6efa8d64dafbe7..8777c8f48e0f53 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 5789d37a6bcf6c..80a2f9d7709cc5 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 665114bd86e9bd..9e57d7ae608cc4 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 9e802b96e16be3..1dfd9e3edc2c5a 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 244d431930f2eb..a282fa08e8f38c 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 00000000000000..0b8a9349ad5fb3 --- /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 00000000000000..e280fd3f34e996 --- /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 463d170df22960..838b684cc10e8f 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 3a25417f7db4c9..66a7ac25d4a702 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 9deeeb96124c8b..799886d264547e 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 0bec188f636406..da332aa326ccf5 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 6000af5498cd6e..841415620d6917 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 aeb5801c9f6b50..b7eafb7bf5c880 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx @@ -14,7 +14,9 @@ export const LEVEL_MAP: { [level: string]: number } = { critical: 1, }; -export const REVERSE_LEVEL_MAP: { [idx: number]: DeprecationInfo['level'] } = invert(LEVEL_MAP); +export const REVERSE_LEVEL_MAP: { [idx: number]: DeprecationInfo['level'] } = invert( + LEVEL_MAP +) as any; export const COLOR_MAP: { [level: string]: IconColor } = { warning: 'default', diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx index 25cf400bcd0fdb..1c587568fe61dd 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 bbcba7238748d1..55a99ab8541f89 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 12fff376a1a3cd..26d0093446461b 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 a9d04e68b8929f..f80c73dcf5bb0e 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx @@ -7,7 +7,7 @@ import React from 'react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { capitalize } from 'lodash'; +import { upperFirst } from 'lodash'; import styled from 'styled-components'; import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { parseTimestamp } from './parse_timestamp'; @@ -83,9 +83,9 @@ export const getLocationStatus = (checks: Check[], status: string) => { const location = check?.observer?.geo?.name ?? UNNAMED_LOCATION; if (check.monitor.status === STATUS.UP) { - upChecks.add(capitalize(location)); + upChecks.add(upperFirst(location)); } else if (check.monitor.status === STATUS.DOWN) { - downChecks.add(capitalize(location)); + downChecks.add(upperFirst(location)); } }); diff --git a/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index 3a940b4655b197..d6185f2c2589a3 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 414b37939b3816..397d23a18332c9 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/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 00000000000000..dd7996b68c41f5 --- /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 6e52b3a11f25f5..f5c4c55a4e3000 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 b82f631a30fe7c..01702a033d5852 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 9dc162e06fba08..189da025e7142b 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 2bdd03e23c6dcd..90cb65a77e9ae8 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/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index eeca8ee54e32f3..ace32111005f4c 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -246,6 +246,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC percentWithRelated: 100, numTrees: 1, alwaysGenMaxChildrenPerNode: true, + ancestryArraySize: 2, }; describe('Resolver', () => { @@ -542,13 +543,10 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('returns multiple levels of child process lifecycle events', async () => { const { body }: { body: ResolverChildren } = await supertest - .get( - `/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&generations=1` - ) + .get(`/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&children=10`) .expect(200); + expect(body.childNodes.length).to.eql(10); expect(body.nextChild).to.be(null); - expect(body.childNodes[0].nextChild).to.be(null); - expect(body.childNodes.length).to.eql(8); expect(body.childNodes[0].lifecycle.length).to.eql(1); expect( // for some reason the ts server doesn't think `endgame` exists even though we're using ResolverEvent @@ -615,19 +613,27 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.childNodes.length).to.eql(12); // there will be 4 parents, the origin of the tree, and it's 3 children verifyChildren(body.childNodes, tree, 4, 3); + expect(body.nextChild).to.eql(null); }); it('returns a single generation of children', async () => { + // this gets a node should have 3 children which were created in succession so that the timestamps + // are ordered correctly to be retrieved in a single call + const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; const { body }: { body: ResolverChildren } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/children?generations=1`) + .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=3`) .expect(200); expect(body.childNodes.length).to.eql(3); verifyChildren(body.childNodes, tree, 1, 3); + expect(body.nextChild).to.not.eql(null); }); - it('paginates the children of the origin node', async () => { + it('paginates the children', async () => { + // this gets a node should have 3 children which were created in succession so that the timestamps + // are ordered correctly to be retrieved in a single call + const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; let { body }: { body: ResolverChildren } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/children?generations=1&children=1`) + .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=1`) .expect(200); expect(body.childNodes.length).to.eql(1); verifyChildren(body.childNodes, tree, 1, 1); @@ -635,49 +641,41 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ({ body } = await supertest .get( - `/api/endpoint/resolver/${tree.origin.id}/children?generations=1&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${distantChildEntityID}/children?children=2&afterChild=${body.nextChild}` ) .expect(200)); expect(body.childNodes.length).to.eql(2); verifyChildren(body.childNodes, tree, 1, 2); - expect(body.childNodes[0].nextChild).to.be(null); - expect(body.childNodes[1].nextChild).to.be(null); - }); - - it('paginates the children of different nodes', async () => { - let { body }: { body: ResolverChildren } = await supertest - .get(`/api/endpoint/resolver/${tree.origin.id}/children?generations=2&children=2`) - .expect(200); - // it should return 4 nodes total, 2 for each level - expect(body.childNodes.length).to.eql(4); - verifyChildren(body.childNodes, tree, 2); expect(body.nextChild).to.not.be(null); - expect(body.childNodes[0].nextChild).to.not.be(null); - // the second child will not have any results returned for it so it should not have pagination set (the first) - // request to get it's children should start at the beginning aka not passing any pagination parameter - expect(body.childNodes[1].nextChild).to.be(null); - const firstChild = body.childNodes[0]; - - // get the 3rd child of the origin of the tree ({ body } = await supertest .get( - `/api/endpoint/resolver/${tree.origin.id}/children?generations=1&children=10&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${distantChildEntityID}/children?children=2&afterChild=${body.nextChild}` ) .expect(200)); - expect(body.childNodes.length).to.be(1); - verifyChildren(body.childNodes, tree, 1, 1); - expect(body.childNodes[0].nextChild).to.be(null); + expect(body.childNodes.length).to.eql(0); + expect(body.nextChild).to.be(null); + }); + + it('gets all children in two queries', async () => { + // should get all the children of the origin + let { body }: { body: ResolverChildren } = await supertest + .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=3`) + .expect(200); + expect(body.childNodes.length).to.eql(3); + verifyChildren(body.childNodes, tree); + expect(body.nextChild).to.not.be(null); + const firstNodes = [...body.childNodes]; - // get the 1 child of the origin of the tree's last child ({ body } = await supertest .get( - `/api/endpoint/resolver/${firstChild.entityID}/children?generations=1&children=10&afterChild=${firstChild.nextChild}` + `/api/endpoint/resolver/${tree.origin.id}/children?children=10&afterChild=${body.nextChild}` ) .expect(200)); - expect(body.childNodes.length).to.be(1); - verifyChildren(body.childNodes, tree, 1, 1); - expect(body.childNodes[0].nextChild).to.be(null); + expect(body.childNodes.length).to.eql(9); + // put all the results together and we should have all the children + verifyChildren([...firstNodes, ...body.childNodes], tree, 4, 3); + expect(body.nextChild).to.be(null); }); }); }); @@ -703,7 +701,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('returns a tree', async () => { const { body }: { body: ResolverTree } = await supertest .get( - `/api/endpoint/resolver/${tree.origin.id}?children=100&generations=3&ancestors=5&events=4&alerts=4` + `/api/endpoint/resolver/${tree.origin.id}?children=100&ancestors=5&events=5&alerts=5` ) .expect(200); 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 71057b81d1b097..a6a4003a554fcb 100644 --- a/x-pack/test/api_integration/apis/fleet/agent_flow.ts +++ b/x-pack/test/api_integration/apis/fleet/agent_flow.ts @@ -18,7 +18,7 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); const esClient = getService('es'); - describe('fleet_agent_flow', () => { + describe.skip('fleet_agent_flow', () => { before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index df6c61422c4ce2..3a3d73ab68412b 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 @@ -25,8 +25,7 @@ export default function ({ getService }) { cleanUpTemplates, } = registerHelpers({ supertest }); - // blocking es snapshot promotion: https://github.com/elastic/kibana/issues/70532 - describe.skip('index templates', () => { + describe('index templates', () => { after(() => Promise.all([cleanUpEsResources(), cleanUpTemplates()])); describe('get all', () => { 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 673ee3c3f84756..23b0a96ecd401f 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 554b17d505c5ea..d372496d2d1d96 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 1f4da602b108a8..bb0934b73a4c7d 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/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 1c98cd3a4e379b..10c0f00234abc5 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 de0abe2350eb5b..78915f6580299f 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 00bfcdc119e475..d2bfdbe4dc967b 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/api_integration/apis/telemetry/fixtures/basiccluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json index a0097f53ac93b9..74d91a6215c795 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json @@ -153,9 +153,7 @@ "min": 223 } }, - "versions": [ - "6.3.1" - ] + "versions": ["6.3.1"] }, "status": "yellow", "timestamp": 1532386499084 @@ -297,9 +295,7 @@ }, "audit": { "enabled": false, - "outputs": [ - "logfile" - ] + "outputs": ["logfile"] }, "available": false, "enabled": true, diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json index 6cc9c55157b282..7d408e39247ee4 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json @@ -92,9 +92,7 @@ "master": 1, "ingest": 1 }, - "versions": [ - "7.0.0-alpha1" - ], + "versions": ["7.0.0-alpha1"], "os": { "available_processors": 4, "allocated_processors": 1, @@ -214,9 +212,7 @@ } }, "audit": { - "outputs": [ - "logfile" - ], + "outputs": ["logfile"], "enabled": false }, "ipfilter": { @@ -383,9 +379,7 @@ "master": 1, "ingest": 1 }, - "versions": [ - "7.0.0-alpha1" - ], + "versions": ["7.0.0-alpha1"], "os": { "available_processors": 4, "allocated_processors": 1, @@ -461,34 +455,22 @@ "enabled": true, "realms": { "file": { - "name": [ - "default_file" - ], + "name": ["default_file"], "available": true, - "size": [ - 0 - ], + "size": [0], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "ldap": { "available": true, "enabled": false }, "native": { - "name": [ - "default_native" - ], + "name": ["default_native"], "available": true, - "size": [ - 2 - ], + "size": [2], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "active_directory": { "available": true, @@ -523,9 +505,7 @@ } }, "audit": { - "outputs": [ - "logfile" - ], + "outputs": ["logfile"], "enabled": false }, "ipfilter": { @@ -700,9 +680,7 @@ "master": 2, "ingest": 2 }, - "versions": [ - "7.0.0-alpha1" - ], + "versions": ["7.0.0-alpha1"], "os": { "available_processors": 8, "allocated_processors": 2, @@ -778,34 +756,22 @@ "enabled": true, "realms": { "file": { - "name": [ - "default_file" - ], + "name": ["default_file"], "available": true, - "size": [ - 0 - ], + "size": [0], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "ldap": { "available": true, "enabled": false }, "native": { - "name": [ - "default_native" - ], + "name": ["default_native"], "available": true, - "size": [ - 1 - ], + "size": [1], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "active_directory": { "available": true, @@ -840,9 +806,7 @@ } }, "audit": { - "outputs": [ - "logfile" - ], + "outputs": ["logfile"], "enabled": false }, "ipfilter": { diff --git a/x-pack/test/api_integration/services/resolver.ts b/x-pack/test/api_integration/services/resolver.ts index 7a100c37aea915..750d2f702fb843 100644 --- a/x-pack/test/api_integration/services/resolver.ts +++ b/x-pack/test/api_integration/services/resolver.ts @@ -18,6 +18,7 @@ export interface Options extends TreeOptions { * Number of trees to generate. */ numTrees?: number; + seed?: string; } /** @@ -38,8 +39,9 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { eventsIndex: string = 'logs-endpoint.events.process-default', alertsIndex: string = 'logs-endpoint.alerts-default' ): Promise { + const seed = options.seed || 'resolver-seed'; const allTrees: Tree[] = []; - const generator = new EndpointDocGenerator(); + const generator = new EndpointDocGenerator(seed); const numTrees = options.numTrees ?? 1; for (let j = 0; j < numTrees; j++) { const tree = generator.generateTree(options); 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 cd78f0ff7b88d8..662879c495230a 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 b5b0197aad4b32..eab90e1fc19cff 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 b980116c581daf..9146ec73346258 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 b399c9e915e27a..97cdd081705a44 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/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js index 68d3c2536ee0b6..5d75679432c979 100644 --- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js @@ -76,7 +76,11 @@ export default function ({ getPageObjects, getService }) { const { lat, lon, zoom } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(41); expect(Math.round(lon)).to.equal(-102); - expect(Math.round(zoom)).to.equal(5); + + // Centering is correct, but screen-size and dpi affect zoom level, + // causing this test to be brittle in different environments + // Expecting zoom-level to be between ]4,5] + expect(Math.ceil(zoom)).to.equal(5); }); }); 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 c0a1ff34db96b4..72f463be48fd53 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); @@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }) { global: ['all'], }, }); - const roles = indexBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); + const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); log.debug('actualRoles = %j', roles); expect(roles).to.have.key('myroleEast'); expect(roles.myroleEast.reserved).to.be(false); @@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: ['kibana_admin', 'myroleEast'], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.userEast.roles).to.eql(['kibana_admin', 'myroleEast']); expect(users.userEast.reserved).to.be(false); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index ec6b91219b7c47..7b22d72885c9d2 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 e9c09d1af6ea67..b138859d01361c 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 94b4306819052f..2054a7b0b00380 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 b6e3a84d6f7c00..a2a2b705172d7e 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 4c8c04c0356b0d..4fd4384a93c59f 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['security', 'settings']); const config = getService('config'); @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }) { }); it('should show the default elastic and kibana_system users', async function () { - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.info('actualUsers = %j', users); log.info('config = %j', config.get('servers.elasticsearch.hostname')); if (config.get('servers.elasticsearch.hostname') === 'localhost') { @@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: ['kibana_admin'], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.Lee.roles).to.eql(['kibana_admin']); expect(users.Lee.fullname).to.eql('LeeFirst LeeLast'); @@ -66,7 +66,7 @@ export default function ({ getService, getPageObjects }) { save: true, roles: [], }); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users.OptionalUser.roles).to.eql(['']); expect(users.OptionalUser.fullname).to.eql(''); @@ -77,14 +77,14 @@ export default function ({ getService, getPageObjects }) { it('should delete user', async function () { const alertMsg = await PageObjects.security.deleteUser('Lee'); log.debug('alertMsg = %s', alertMsg); - const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); expect(users).to.not.have.key('Lee'); }); it('should show the default roles', async function () { await PageObjects.security.clickElasticsearchRoles(); - const roles = indexBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); + const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); log.debug('actualRoles = %j', roles); // This only contains the first page of alphabetically sorted results, so the assertions are only for the first handful of expected roles. expect(roles.apm_system.reserved).to.be(true); diff --git a/x-pack/test/functional/apps/watcher/watcher_test.js b/x-pack/test/functional/apps/watcher/watcher_test.js index 7a2eebc118ab37..1dd3fb6bbcc3d1 100644 --- a/x-pack/test/functional/apps/watcher/watcher_test.js +++ b/x-pack/test/functional/apps/watcher/watcher_test.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { indexBy } from 'lodash'; +import { keyBy } from 'lodash'; const watchID = 'watchID'; const watchName = 'watch Name'; @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }) { it('should delete the watch', async () => { // Navigate to the main list page await PageObjects.common.navigateToApp('watcher'); - const watchList = indexBy(await PageObjects.watcher.getWatches(), 'id'); + const watchList = keyBy(await PageObjects.watcher.getWatches(), 'id'); log.debug(watchList); expect(watchList.watchID.name).to.eql([watchName]); await PageObjects.watcher.deleteWatch(watchID); diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index c317aad8ba05b5..b3d49199b0d9eb 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 7c479a4234673e..80df235bf6ff8d 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 13bf47676cc09c..2225316bba80f4 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 e22b098e6ee0c2..d86d272c1da8c8 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 35a420561731ae..2b174a8cae076e 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 9fbe9f26944ca5..ba6d7ced3c591a 100644 --- a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts +++ b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/plugin.ts @@ -265,8 +265,8 @@ function avg(items: number[]) { return { mean: Math.round(stats.mean(items)), range: { - min: Math.round(isNumericArray(mode) ? _.min([...mode]) : mode), - max: Math.round(isNumericArray(mode) ? _.max([...mode]) : mode), + min: Math.round(isNumericArray(mode) ? (_.min([...mode]) as number) : (mode as number)), + max: Math.round(isNumericArray(mode) ? (_.max([...mode]) as number) : (mode as number)), }, }; } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 45ea82c59bf978..bacba619e5648e 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/typings/index.d.ts b/x-pack/typings/index.d.ts index 1bf1370ad467fa..73efee0dab2eb7 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 ee61303e85f4a9..02f7e90ab7d24b 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.6.3": + version "19.6.3" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.6.3.tgz#c23a1d7a8e245b1a800a3a4ef5fc4378b0da5e74" + integrity sha512-lB+rOODUKYZvsWCAcCxtAu8UxdZ2yIjZs+cjXwO1SlngY+jo+gc6XoEZG4kAczRPcr6cMdHesZ8LmFr3Enle5Q== 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"