diff --git a/.bazelrc.common b/.bazelrc.common index 20a41c4cde9a0..115c0214b1a53 100644 --- a/.bazelrc.common +++ b/.bazelrc.common @@ -10,12 +10,13 @@ build --experimental_guard_against_concurrent_changes run --experimental_guard_against_concurrent_changes test --experimental_guard_against_concurrent_changes +query --experimental_guard_against_concurrent_changes ## Cache action outputs on disk so they persist across output_base and bazel shutdown (eg. changing branches) -build --disk_cache=~/.bazel-cache/disk-cache +common --disk_cache=~/.bazel-cache/disk-cache ## Bazel repo cache settings -build --repository_cache=~/.bazel-cache/repository-cache +common --repository_cache=~/.bazel-cache/repository-cache # Bazel will create symlinks from the workspace directory to output artifacts. # Build results will be placed in a directory called "bazel-bin" @@ -35,13 +36,16 @@ build --experimental_inprocess_symlink_creation # Incompatible flags to run with build --incompatible_no_implicit_file_export build --incompatible_restrict_string_escapes +query --incompatible_no_implicit_file_export +query --incompatible_restrict_string_escapes # Log configs ## different from default common --color=yes -build --show_task_finish -build --noshow_progress +common --noshow_progress +common --show_task_finish build --noshow_loading_progress +query --noshow_loading_progress build --show_result=0 # Specifies desired output mode for running tests. @@ -82,7 +86,7 @@ test:debug --test_output=streamed --test_strategy=exclusive --test_timeout=9999 run:debug --define=VERBOSE_LOGS=1 -- --node_options=--inspect-brk # The following option will change the build output of certain rules such as terser and may not be desirable in all cases # It will also output both the repo cache and action cache to a folder inside the repo -build:debug --compilation_mode=dbg --show_result=1 +build:debug --compilation_mode=dbg --show_result=0 --noshow_loading_progress --noshow_progress --show_task_finish # Turn off legacy external runfiles # This prevents accidentally depending on this feature, which Bazel will remove. diff --git a/NOTICE.txt b/NOTICE.txt index 2341a478cbda9..4eec329b7a603 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -261,33 +261,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -This product bundles childnode-remove which is available under a -"MIT" license. - -The MIT License (MIT) - -Copyright (c) 2016-present, jszhou -https://github.com/jserz/js_piece - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - --- This product bundles code based on probot-metadata@1.0.0 which is available under a "MIT" license. diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index e74c646eedeaf..bd4d8801b0d4e 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -52,9 +52,6 @@ node_repositories( # NOTE: FORCE_COLOR env var forces colors on non tty mode yarn_install( name = "npm", - environment = { - "FORCE_COLOR": "True", - }, package_json = "//:package.json", yarn_lock = "//:yarn.lock", data = [ diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 71c05052c018e..7dabd3939a6d8 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -1101,7 +1101,64 @@ Uptime:: For detailed information about the 7.10.0 release, review the following sections. -<> | <> | <> | <> +<> | <> | <> | <> + +[float] +[[deprecation-v7.10.0]] +=== Deprecations +Monitoring:: +* "Internal Monitoring" deprecation warning {kibana-pull}72020[#72020] +Platform:: +* The /api/status endpoint response format is now deprecated and will change in 8.0 {kibana-pull}76054[#76054] +* These two config keys have been renamed and the old names will no longer work as of 8.0: cpu.cgroup.path.override => ops.cGroupOverrides.cpuPath and cpuacct.cgroup.path.override => ops.cGroupOverrides.cpuAcctPath {kibana-pull}76730[#76730] +Security:: +* Deprecates the `xpack.security.authc.providers.saml..maxRedirectURLSize` setting for SAML authentication {kibana-pull}68117[#68117] +Visualizations:: +* In 7.0 and later, *Timelion* app is deprecated. In 8.0 and later, *Timelion* app is removed from {kib} {kibana-pull}74660[#74660] ++ +To prepare for the removal of *Timelion* app, you must migrate *Timelion* app worksheets to a dashboard. ++ +NOTE: Only *Timelion* app is deprecated. {kib} continues to support *Timelion* +visualizations in *Dashboard*, *Visualize*, and *Canvas*. ++ +To migrate a *Timelion* worksheet to a dashboard: + +. Open the main menu, click *Dashboard*, then click *Create dashboard*. + +. For each *Timelion* app worksheet, complete the following steps. + +.. On the dashboard, click *Create New*, then click *Timelion* on the *New Visualization* window. + +.. Open a new tab, open the *Timelion* app, select the chart you want to copy, then copy the chart expression. ++ +[role="screenshot"] +image::images/timelion-copy-expression.png[Timelion app chart] + +.. Go to *Timelion*, paste the chart expression in the *Timelion expression* field, then click *Update*. ++ +[role="screenshot"] +image::images/timelion-vis-paste-expression.png[Timelion advanced editor UI] + +.. In the toolbar, click *Save*. + +.. On the *Save visualization* window, enter the visualization *Title*, then click *Save and return*. ++ +The Timelion visualization panel appears on the dashboard. ++ +[role="screenshot"] +image::images/timelion-dashboard.png[Final dashboard with saved Timelion app worksheet] + +[float] +[[breaking-v7.10.0]] +=== Breaking changes +Lens and visualizations:: +* Deprecates schema-less specs in Vega {kibana-pull}73805[#73805] +Operations:: +* The bin/kibana-plugin CLI has been updated to work with the new Kibana Platform plugin format instead of the legacy plugin format {kibana-pull}74604[#74604] +Platform:: +* The legacy plugin system and the legacy plugin API have been removed. It is no longer possible to use third parties legacy Kibana plugins. Legacy plugin owners should migrate their plugins to the Kibana Platform plugin API {kibana-pull}77599[#77599] + +For more information, refer to <>. [float] [[enhancement-v7.10.0]] @@ -1532,61 +1589,6 @@ Uptime:: * Ml anomaly alert edit {kibana-pull}76909[#76909] * Removes custom handling of license enabling {kibana-pull}82019[#82019] -[float] -[[deprecation-v7.10.0]] -=== Deprecations -Monitoring:: -* "Internal Monitoring" deprecation warning {kibana-pull}72020[#72020] -Platform:: -* The /api/status endpoint response format is now deprecated and will change in 8.0. {kibana-pull}76054[#76054] -* These two config keys have been renamed and the old names will no longer work as of 8.0: cpu.cgroup.path.override => ops.cGroupOverrides.cpuPath and cpuacct.cgroup.path.override => ops.cGroupOverrides.cpuAcctPath {kibana-pull}76730[#76730] -Visualizations:: -* In 7.0 and later, *Timelion* app is deprecated. In 8.0 and later, *Timelion* app is removed from {kib}. {kibana-pull}74660[#74660] -+ -To prepare for the removal of *Timelion* app, you must migrate *Timelion* app worksheets to a dashboard. -+ -NOTE: Only *Timelion* app is deprecated. {kib} continues to support *Timelion* -visualizations in *Dashboard*, *Visualize*, and *Canvas*. -+ -To migrate a *Timelion* worksheet to a dashboard: - -. Open the main menu, click *Dashboard*, then click *Create dashboard*. - -. For each *Timelion* app worksheet, complete the following steps. - -.. On the dashboard, click *Create New*, then click *Timelion* on the *New Visualization* window. - -.. Open a new tab, open the *Timelion* app, select the chart you want to copy, then copy the chart expression. -+ -[role="screenshot"] -image::images/timelion-copy-expression.png[Timelion app chart] - -.. Go to *Timelion*, paste the chart expression in the *Timelion expression* field, then click *Update*. -+ -[role="screenshot"] -image::images/timelion-vis-paste-expression.png[Timelion advanced editor UI] - -.. In the toolbar, click *Save*. - -.. On the *Save visualization* window, enter the visualization *Title*, then click *Save and return*. -+ -The Timelion visualization panel appears on the dashboard. -+ -[role="screenshot"] -image::images/timelion-dashboard.png[Final dashboard with saved Timelion app worksheet] - -[float] -[[breaking-v7.10.0]] -=== Breaking changes -Lens and visualizations:: -* Deprecates schema-less specs in Vega {kibana-pull}73805[#73805] -Operations:: -* The bin/kibana-plugin CLI has been updated to work with the new Kibana Platform plugin format instead of the legacy plugin format {kibana-pull}74604[#74604] -Platform:: -* The legacy plugin system and the legacy plugin API have been removed. It is no longer possible to use third parties legacy Kibana plugins. Legacy plugin owners should migrate their plugins to the Kibana Platform plugin API {kibana-pull}77599[#77599] - -For more information, refer to <>. - [[release-notes-7.9.3]] == {kib} 7.9.3 @@ -2470,6 +2472,34 @@ By default, the Upgrade Assistant flaw is mitigated in all {kib} instances acces + For all other installations, you must upgrade to 7.7.0. If you are unable to upgrade, set `metrics.enabled:false` in your kibana.yml file to disable TSVB. +[float] +[[deprecations-7.7.0]] +=== Deprecations + +The following functionality is deprecated in 7.7.0, and will be removed in 8.0.0. +Deprecated functionality does not have an immediate impact on your application, but we strongly recommend +you make the necessary updates after you upgrade to 7.7.0. + +[discrete] +[[deprecation-53010]] +==== Deprecates authentication settings + +*Details* + + +Deprecates the `xpack.security.authc.saml.realm` and `xpack.security.authc.saml.maxRedirectURLSize` settings used for {kib} SAML authentication. + +Deprecates the `xpack.security.authc.oidc.realm` settings used for {kib} OpenID Connect authentication. + +For more information, refer to {kibana-pull}53010[#53010]. + +*Impact* + + +* Instead of `xpack.security.authc.saml.realm`, use `xpack.security.authc.providers.saml..realm`. + +* Instead of `xpack.security.authc.saml.maxRedirectURLSize`, use `xpack.security.authc.providers.saml..maxRedirectURLSize`. + +* Instead of `xpack.security.authc.oidc.realm`, use `xpack.security.authc.providers.oidc..realm`. + [float] [[known-issues-7.7.0]] === Known issues @@ -4143,6 +4173,29 @@ Machine Learning:: Maps:: * [Fixes #34662] Hide feature when it has no corresponding term join {kib-pull}36617[#36617] + +[float] +[[deprecations-7.3.0]] +=== Deprecation + +The following functionality is deprecated in 7.3.0, and will be removed in 8.0.0. +Deprecated functionality does not have an immediate impact on your application, but we strongly recommend +you make the necessary updates after you upgrade to 7.3.0. + +[discrete] +[[deprecation-xpack-security-authProviders]] +==== Deprecates `xpack.security.authProviders` and `xpack.security.public` + +*Details:* Deprecates the `xpack.security.authProviders` and `xpack.security.public` settings used for {kib} SAML authentication. For more information, refer to {pull}37346[#37346]. + +*Impact:* + +When you use the SAML authentication provider, use the following settings: + +* Instead of `xpack.security.authProviders`, use `xpack.security.authc.providers`. + +* Instead of `xpack.security.public`, use `xpack.security.authc.saml.realm`. + [float] [[enhancement-7.3.0]] === Enhancements diff --git a/docs/developer/getting-started/index.asciidoc b/docs/developer/getting-started/index.asciidoc index 7a8ec9bb03c73..f7317b0fd4b53 100644 --- a/docs/developer/getting-started/index.asciidoc +++ b/docs/developer/getting-started/index.asciidoc @@ -66,7 +66,8 @@ yarn kbn bootstrap --force-install (You can also run `yarn kbn` to see the other available commands. For more info about this tool, see -{kib-repo}tree/{branch}/packages/kbn-pm[{kib-repo}tree/{branch}/packages/kbn-pm].) +{kib-repo}tree/{branch}/packages/kbn-pm[{kib-repo}tree/{branch}/packages/kbn-pm]. If you want more +information about how to actively develop over packages please read <>) When switching branches which use different versions of npm packages you may need to run: @@ -169,3 +170,5 @@ include::debugging.asciidoc[leveloffset=+1] include::building-kibana.asciidoc[leveloffset=+1] include::development-plugin-resources.asciidoc[leveloffset=+1] + +include::monorepo-packages.asciidoc[leveloffset=+1] diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc new file mode 100644 index 0000000000000..a95b357570278 --- /dev/null +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -0,0 +1,66 @@ +[[monorepo-packages]] +== {kib} Monorepo Packages + +Currently {kib} works as a monorepo composed by a core, plugins and packages. +The latest are located in a folder called `packages` and are pieces of software that +composes a set of features that can be isolated and reused across the entire repository. +They are also supposed to be able to imported just like any other `node_module`. + +Previously we relied solely on `@kbn/pm` to manage the development tools of those packages, but we are +now in the middle of migrating those responsibilities into Bazel. Every package already migrated +will contain in its root folder a `BUILD.bazel` file and other `build` and `watching` strategies should be used. + +Remember that any time you need to make sure the monorepo is ready to be used just run: + +[source,bash] +---- +yarn kbn bootstrap +---- + +[discrete] +=== Building Non Bazel Packages + +Non Bazel packages can be built independently with + +[source,bash] +---- +yarn kbn run build -i PACKAGE_NAME +---- + +[discrete] +=== Watching Non Bazel Packages + +Non Bazel packages can be watched independently with + +[source,bash] +---- +yarn kbn watch -i PACKAGE_NAME +---- + +[discrete] +=== Building Bazel Packages + +Bazel packages are built as a whole for now. You can use: + +[source,bash] +---- +yarn kbn build-bazel +---- + +[discrete] +=== Watching Bazel Packages + +Bazel packages are watched as a whole for now. You can use: + +[source,bash] +---- +yarn kbn watch-bazel +---- + + +[discrete] +=== List of Already Migrated Packages to Bazel + +- @elastic/datemath + + diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 6ca7a83ac0a03..860f7c3c74892 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -144,6 +144,7 @@ readonly links: { putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putWatch: string; + simulatePipeline: string; updateTransform: string; }>; readonly observability: Record; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 3847ab0c6183a..a9cb6729b214e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
simulatePipeline: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md new file mode 100644 index 0000000000000..c69850006e146 --- /dev/null +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableStart](./kibana-plugin-plugins-embeddable-server.embeddablestart.md) + +## EmbeddableStart type + +Signature: + +```typescript +export declare type EmbeddableStart = PersistableStateService; +``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md index 19ee57d677250..5b3083e039847 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md @@ -18,3 +18,9 @@ | --- | --- | | [plugin](./kibana-plugin-plugins-embeddable-server.plugin.md) | | +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [EmbeddableStart](./kibana-plugin-plugins-embeddable-server.embeddablestart.md) | | + diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 88dbf6ec8761f..3d9253025d3cc 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -125,6 +125,11 @@ pattern: *:logstash-* ``` +You can use exclusions to exclude indices that might contain mapping errors. +To match indices starting with `logstash-`, and exclude those starting with `logstash-old` from +all clusters having a name starting with `cluster_`, you can use `cluster_*:logstash-*,cluster*:logstash-old*`. +To exclude a cluster, use `cluster_*:logstash-*,cluster_one:-*`. + Once an index pattern is configured using the {ccs} syntax, all searches and aggregations using that index pattern in {kib} take advantage of {ccs}. diff --git a/docs/settings/dev-settings.asciidoc b/docs/settings/dev-settings.asciidoc index 62553293a7d03..810694f46b317 100644 --- a/docs/settings/dev-settings.asciidoc +++ b/docs/settings/dev-settings.asciidoc @@ -29,3 +29,14 @@ They are enabled by default. | Set to `true` to enable the <>. Defaults to `true`. |=== + +[float] +[[painless_lab-settings]] +==== Painless Lab settings + +[cols="2*<"] +|=== +| `xpack.painless_lab.enabled` + | When set to `true`, enables the <>. Defaults to `true`. + +|=== diff --git a/docs/settings/search-sessions-settings.asciidoc b/docs/settings/search-sessions-settings.asciidoc index cf64d08e4806c..abd6a8f12b568 100644 --- a/docs/settings/search-sessions-settings.asciidoc +++ b/docs/settings/search-sessions-settings.asciidoc @@ -11,15 +11,33 @@ Configure the search session settings in your `kibana.yml` configuration file. [cols="2*<"] |=== a| `xpack.data_enhanced.` -`search.sessions.enabled` +`search.sessions.enabled` {ess-icon} | Set to `true` (default) to enable search sessions. a| `xpack.data_enhanced.` -`search.sessions.trackingInterval` -| The frequency for updating the state of a search session. The default is 10s. +`search.sessions.trackingInterval` {ess-icon} +| The frequency for updating the state of a search session. The default is `10s`. a| `xpack.data_enhanced.` -`search.sessions.defaultExpiration` +`search.sessions.pageSize` {ess-icon} +| How many search sessions {kib} processes at once while monitoring +session progress. The default is `100`. + +a| `xpack.data_enhanced.` +`search.sessions.notTouchedTimeout` {ess-icon} +| How long {kib} stores search results from unsaved sessions, +after the last search in the session completes. The default is `5m`. + +a| `xpack.data_enhanced.` +`search.sessions.notTouchedInProgressTimeout` {ess-icon} +| How long a search session can run after a user navigates away without saving a session. The default is `1m`. + +a| `xpack.data_enhanced.` +`search.sessions.maxUpdateRetries` {ess-icon} +| How many retries {kib} can perform while attempting to save a search session. The default is `3`. + +a| `xpack.data_enhanced.` +`search.sessions.defaultExpiration` {ess-icon} | How long search session results are stored before they are deleted. -Extending a search session resets the expiration by the same value. The default is 7d. +Extending a search session resets the expiration by the same value. The default is `7d`. |=== diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc index 63839cf465e98..05885f1af13ba 100644 --- a/docs/user/alerting/defining-rules.asciidoc +++ b/docs/user/alerting/defining-rules.asciidoc @@ -28,8 +28,8 @@ Name:: The name of the rule. While this name does not have to be unique, th Tags:: A list of tag names that can be applied to a rule. Tags can help you organize and find rules, because tags appear in the rule listing in the management UI which is searchable by tag. Check every:: This value determines how frequently the rule conditions below are checked. Note that the timing of background rule checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. Notify:: This value limits how often actions are repeated when an alert remains active across rule checks. See <> for more information. + -- **Only on status change**: Actions are not repeated when an alert remains active across checks. Actions run only when the rule status changes. -- **Every time rule is active**: Actions are repeated when an alert remains active across checks. +- **Only on status change**: Actions are not repeated when an alert remains active across checks. Actions run only when the alert status changes. +- **Every time alert is active**: Actions are repeated when an alert remains active across checks. - **On a custom action interval**: Actions are suppressed for the throttle interval, but repeat when an alert remains active across checks for a duration longer than the throttle interval. diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 04f4e986ca289..bbc9c41c6ca5a 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -84,7 +84,7 @@ by running checks on a schedule time of 1 minute with a re-notify interval of 6 This alert is triggered if a large (primary) shard size is found on any of the specified index patterns. The trigger condition is met if an index's shard size is 55gb or higher in the last 5 minutes. The alert is grouped across all indices that match -the default patter of `*` by running checks on a schedule time of 1 minute with a re-notify +the default pattern of `*` by running checks on a schedule time of 1 minute with a re-notify interval of 12 hours. [discrete] diff --git a/examples/dashboard_embeddable_examples/public/app.tsx b/examples/dashboard_embeddable_examples/public/app.tsx index 0e21e4421e742..8a6b5a90a22a8 100644 --- a/examples/dashboard_embeddable_examples/public/app.tsx +++ b/examples/dashboard_embeddable_examples/public/app.tsx @@ -55,7 +55,9 @@ const Nav = withRouter(({ history, pages }: NavProps) => { interface Props { basename: string; - DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer']; + DashboardContainerByValueRenderer: ReturnType< + DashboardStart['getDashboardContainerByValueRenderer'] + >; } const DashboardEmbeddableExplorerApp = ({ basename, DashboardContainerByValueRenderer }: Props) => { diff --git a/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx b/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx index cba87d466176e..29297341c3016 100644 --- a/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx +++ b/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx @@ -96,7 +96,9 @@ const initialInput: DashboardContainerInput = { export const DashboardEmbeddableByValue = ({ DashboardContainerByValueRenderer, }: { - DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer']; + DashboardContainerByValueRenderer: ReturnType< + DashboardStart['getDashboardContainerByValueRenderer'] + >; }) => { const [input, setInput] = useState(initialInput); diff --git a/examples/dashboard_embeddable_examples/public/plugin.tsx b/examples/dashboard_embeddable_examples/public/plugin.tsx index e57c12daaef23..57678f5a2a517 100644 --- a/examples/dashboard_embeddable_examples/public/plugin.tsx +++ b/examples/dashboard_embeddable_examples/public/plugin.tsx @@ -33,8 +33,7 @@ export class DashboardEmbeddableExamples implements Plugin _log__WEBPACK_IMPORTED_MODULE_5__["log"].info(`${chalk__WEBPACK_IMPORTED_MODULE_0___default.a.cyan('[bazel]')} ${line}`))), Object(_kbn_dev_utils_stdio__WEBPACK_IMPORTED_MODULE_3__["observeLines"])(bazelProc.stderr).pipe(Object(rxjs_operators__WEBPACK_IMPORTED_MODULE_2__["tap"])(line => _log__WEBPACK_IMPORTED_MODULE_5__["log"].info(`${chalk__WEBPACK_IMPORTED_MODULE_0___default.a.cyan('[bazel]')} ${line}`)))).subscribe(bazelLogs$); // Wait for process and logs to finish, unsubscribing in the end + const bazelLogSubscription = rxjs__WEBPACK_IMPORTED_MODULE_1__["merge"](Object(_kbn_dev_utils_stdio__WEBPACK_IMPORTED_MODULE_3__["observeLines"])(bazelProc.stdout).pipe(Object(rxjs_operators__WEBPACK_IMPORTED_MODULE_2__["tap"])(line => _log__WEBPACK_IMPORTED_MODULE_5__["log"].info(`${chalk__WEBPACK_IMPORTED_MODULE_0___default.a.cyan(`[${bazelCommandRunner}]`)} ${line}`))), Object(_kbn_dev_utils_stdio__WEBPACK_IMPORTED_MODULE_3__["observeLines"])(bazelProc.stderr).pipe(Object(rxjs_operators__WEBPACK_IMPORTED_MODULE_2__["tap"])(line => _log__WEBPACK_IMPORTED_MODULE_5__["log"].info(`${chalk__WEBPACK_IMPORTED_MODULE_0___default.a.cyan(`[${bazelCommandRunner}]`)} ${line}`)))).subscribe(bazelLogs$); // Wait for process and logs to finish, unsubscribing in the end + + try { + await bazelProc; + } catch { + throw new _errors__WEBPACK_IMPORTED_MODULE_6__["CliError"](`The bazel command that was running failed to complete.`); + } - await bazelProc; await bazelLogs$.toPromise(); await bazelLogSubscription.unsubscribe(); } +async function runBazel(bazelArgs, offline = false, runOpts = {}) { + await runBazelCommandWithRunner('bazel', bazelArgs, offline, runOpts); +} +async function runIBazel(bazelArgs, offline = false, runOpts = {}) { + await runBazelCommandWithRunner('ibazel', bazelArgs, offline, runOpts); +} + /***/ }), /* 377 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -54550,6 +54574,36 @@ exports.observeReadable = observeReadable; /* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BuildBazelCommand", function() { return BuildBazelCommand; }); +/* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(372); +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const BuildBazelCommand = { + description: 'Runs a build in the Bazel built packages', + name: 'build-bazel', + + async run(projects, projectGraph, { + options + }) { + const runOffline = (options === null || options === void 0 ? void 0 : options.offline) === true; // Call bazel with the target to build all available packages + + await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_0__["runBazel"])(['build', '//packages:build', '--show_result=1'], runOffline); + } + +}; + +/***/ }), +/* 479 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); @@ -54557,7 +54611,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(479); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(480); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); @@ -54660,20 +54714,20 @@ const CleanCommand = { }; /***/ }), -/* 479 */ +/* 480 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(480); -const chalk = __webpack_require__(481); -const cliCursor = __webpack_require__(488); -const cliSpinners = __webpack_require__(490); -const logSymbols = __webpack_require__(492); -const stripAnsi = __webpack_require__(502); -const wcwidth = __webpack_require__(504); -const isInteractive = __webpack_require__(508); -const MuteStream = __webpack_require__(509); +const readline = __webpack_require__(481); +const chalk = __webpack_require__(482); +const cliCursor = __webpack_require__(489); +const cliSpinners = __webpack_require__(491); +const logSymbols = __webpack_require__(493); +const stripAnsi = __webpack_require__(503); +const wcwidth = __webpack_require__(505); +const isInteractive = __webpack_require__(509); +const MuteStream = __webpack_require__(510); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -55026,23 +55080,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 480 */ +/* 481 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 481 */ +/* 482 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(482); +const ansiStyles = __webpack_require__(483); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(486); +} = __webpack_require__(487); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -55243,7 +55297,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(487); + template = __webpack_require__(488); } return template(chalk, parts.join('')); @@ -55272,7 +55326,7 @@ module.exports = chalk; /***/ }), -/* 482 */ +/* 483 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55318,7 +55372,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(483); + colorConvert = __webpack_require__(484); } const offset = isBackground ? 10 : 0; @@ -55443,11 +55497,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 483 */ +/* 484 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(484); -const route = __webpack_require__(485); +const conversions = __webpack_require__(485); +const route = __webpack_require__(486); const convert = {}; @@ -55530,7 +55584,7 @@ module.exports = convert; /***/ }), -/* 484 */ +/* 485 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -56375,10 +56429,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 485 */ +/* 486 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(484); +const conversions = __webpack_require__(485); /* This function routes a model to all other models. @@ -56478,7 +56532,7 @@ module.exports = function (fromModel) { /***/ }), -/* 486 */ +/* 487 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56524,7 +56578,7 @@ module.exports = { /***/ }), -/* 487 */ +/* 488 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56665,12 +56719,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 488 */ +/* 489 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(489); +const restoreCursor = __webpack_require__(490); let isHidden = false; @@ -56707,7 +56761,7 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 489 */ +/* 490 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56723,13 +56777,13 @@ module.exports = onetime(() => { /***/ }), -/* 490 */ +/* 491 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(491)); +const spinners = Object.assign({}, __webpack_require__(492)); const spinnersList = Object.keys(spinners); @@ -56747,18 +56801,18 @@ module.exports.default = spinners; /***/ }), -/* 491 */ +/* 492 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]},\"aesthetic\":{\"interval\":80,\"frames\":[\"▰▱▱▱▱▱▱\",\"▰▰▱▱▱▱▱\",\"▰▰▰▱▱▱▱\",\"▰▰▰▰▱▱▱\",\"▰▰▰▰▰▱▱\",\"▰▰▰▰▰▰▱\",\"▰▰▰▰▰▰▰\",\"▰▱▱▱▱▱▱\"]}}"); /***/ }), -/* 492 */ +/* 493 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(493); +const chalk = __webpack_require__(494); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -56780,16 +56834,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 493 */ +/* 494 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(265); -const ansiStyles = __webpack_require__(494); -const stdoutColor = __webpack_require__(499).stdout; +const ansiStyles = __webpack_require__(495); +const stdoutColor = __webpack_require__(500).stdout; -const template = __webpack_require__(501); +const template = __webpack_require__(502); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -57015,12 +57069,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 494 */ +/* 495 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(495); +const colorConvert = __webpack_require__(496); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -57188,11 +57242,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 495 */ +/* 496 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(496); -var route = __webpack_require__(498); +var conversions = __webpack_require__(497); +var route = __webpack_require__(499); var convert = {}; @@ -57272,11 +57326,11 @@ module.exports = convert; /***/ }), -/* 496 */ +/* 497 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(497); +var cssKeywords = __webpack_require__(498); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -58146,7 +58200,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 497 */ +/* 498 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -58305,10 +58359,10 @@ module.exports = { /***/ }), -/* 498 */ +/* 499 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(496); +var conversions = __webpack_require__(497); /* this function routes a model to all other models. @@ -58408,13 +58462,13 @@ module.exports = function (fromModel) { /***/ }), -/* 499 */ +/* 500 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const os = __webpack_require__(121); -const hasFlag = __webpack_require__(500); +const hasFlag = __webpack_require__(501); const env = process.env; @@ -58546,7 +58600,7 @@ module.exports = { /***/ }), -/* 500 */ +/* 501 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -58561,7 +58615,7 @@ module.exports = (flag, argv) => { /***/ }), -/* 501 */ +/* 502 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -58696,18 +58750,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 502 */ +/* 503 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(503); +const ansiRegex = __webpack_require__(504); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 503 */ +/* 504 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -58724,14 +58778,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 504 */ +/* 505 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(505) -var combining = __webpack_require__(507) +var defaults = __webpack_require__(506) +var combining = __webpack_require__(508) var DEFAULTS = { nul: 0, @@ -58830,10 +58884,10 @@ function bisearch(ucs) { /***/ }), -/* 505 */ +/* 506 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(506); +var clone = __webpack_require__(507); module.exports = function(options, defaults) { options = options || {}; @@ -58848,7 +58902,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 506 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -59020,7 +59074,7 @@ if ( true && module.exports) { /***/ }), -/* 507 */ +/* 508 */ /***/ (function(module, exports) { module.exports = [ @@ -59076,7 +59130,7 @@ module.exports = [ /***/ }), -/* 508 */ +/* 509 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59092,7 +59146,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 509 */ +/* 510 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -59243,7 +59297,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 510 */ +/* 511 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59253,7 +59307,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(479); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(480); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); @@ -59362,16 +59416,18 @@ const ResetCommand = { }; /***/ }), -/* 511 */ +/* 512 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RunCommand", function() { return RunCommand; }); -/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(249); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(247); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); +/* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); +/* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(249); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(246); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(247); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -59383,53 +59439,61 @@ __webpack_require__.r(__webpack_exports__); + const RunCommand = { - description: 'Run script defined in package.json in each package that contains that script.', + description: 'Run script defined in package.json in each package that contains that script (only works on packages not using Bazel yet)', name: 'run', async run(projects, projectGraph, { extraArgs, options }) { - const batchedProjects = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_3__["topologicallyBatchProjects"])(projects, projectGraph); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].warning(dedent__WEBPACK_IMPORTED_MODULE_0___default.a` + We are migrating packages into the Bazel build system and we will no longer support running npm scripts on + packages using 'yarn kbn run' on Bazel built packages. If the package you are trying to act on contains a + BUILD.bazel file please just use 'yarn kbn build-bazel' to build it or 'yarn kbn watch-bazel' to watch it + `); + const batchedProjects = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_4__["topologicallyBatchProjects"])(projects, projectGraph); if (extraArgs.length === 0) { - throw new _utils_errors__WEBPACK_IMPORTED_MODULE_0__["CliError"]('No script specified'); + throw new _utils_errors__WEBPACK_IMPORTED_MODULE_1__["CliError"]('No script specified'); } const scriptName = extraArgs[0]; const scriptArgs = extraArgs.slice(1); - await Object(_utils_parallelize__WEBPACK_IMPORTED_MODULE_2__["parallelizeBatches"])(batchedProjects, async project => { + await Object(_utils_parallelize__WEBPACK_IMPORTED_MODULE_3__["parallelizeBatches"])(batchedProjects, async project => { if (!project.hasScript(scriptName)) { if (!!options['skip-missing']) { return; } - throw new _utils_errors__WEBPACK_IMPORTED_MODULE_0__["CliError"](`[${project.name}] no "${scriptName}" script defined. To skip packages without the "${scriptName}" script pass --skip-missing`); + throw new _utils_errors__WEBPACK_IMPORTED_MODULE_1__["CliError"](`[${project.name}] no "${scriptName}" script defined. To skip packages without the "${scriptName}" script pass --skip-missing`); } - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].info(`[${project.name}] running "${scriptName}" script`); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].info(`[${project.name}] running "${scriptName}" script`); await project.runScriptStreaming(scriptName, { args: scriptArgs }); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`[${project.name}] complete`); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].success(`[${project.name}] complete`); }); } }; /***/ }), -/* 512 */ +/* 513 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "WatchCommand", function() { return WatchCommand; }); -/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(249); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(247); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(513); +/* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); +/* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(249); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(246); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(247); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(514); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -59443,6 +59507,7 @@ __webpack_require__.r(__webpack_exports__); + /** * Name of the script in the package/project package.json file to run during `kbn watch`. */ @@ -59464,10 +59529,14 @@ const kibanaProjectName = 'kibana'; */ const WatchCommand = { - description: 'Runs `kbn:watch` script for every project.', + description: 'Runs `kbn:watch` script for every project (only works on packages not using Bazel yet)', name: 'watch', async run(projects, projectGraph) { + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].warning(dedent__WEBPACK_IMPORTED_MODULE_0___default.a` + We are migrating packages into the Bazel build system. If the package you are trying to watch + contains a BUILD.bazel file please just use 'yarn kbn watch-bazel' + `); const projectsToWatch = new Map(); for (const project of projects.values()) { @@ -59478,33 +59547,33 @@ const WatchCommand = { } if (projectsToWatch.size === 0) { - throw new _utils_errors__WEBPACK_IMPORTED_MODULE_0__["CliError"](`There are no projects to watch found. Make sure that projects define 'kbn:watch' script in 'package.json'.`); + throw new _utils_errors__WEBPACK_IMPORTED_MODULE_1__["CliError"](`There are no projects to watch found. Make sure that projects define 'kbn:watch' script in 'package.json'.`); } const projectNames = Array.from(projectsToWatch.keys()); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].info(`Running ${watchScriptName} scripts for [${projectNames.join(', ')}].`); // Kibana should always be run the last, so we don't rely on automatic + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].info(`Running ${watchScriptName} scripts for [${projectNames.join(', ')}].`); // Kibana should always be run the last, so we don't rely on automatic // topological batching and push it to the last one-entry batch manually. const shouldWatchKibanaProject = projectsToWatch.delete(kibanaProjectName); - const batchedProjects = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_3__["topologicallyBatchProjects"])(projectsToWatch, projectGraph); + const batchedProjects = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_4__["topologicallyBatchProjects"])(projectsToWatch, projectGraph); if (shouldWatchKibanaProject) { batchedProjects.push([projects.get(kibanaProjectName)]); } - await Object(_utils_parallelize__WEBPACK_IMPORTED_MODULE_2__["parallelizeBatches"])(batchedProjects, async pkg => { - const completionHint = await Object(_utils_watch__WEBPACK_IMPORTED_MODULE_4__["waitUntilWatchIsReady"])(pkg.runScriptStreaming(watchScriptName, { + await Object(_utils_parallelize__WEBPACK_IMPORTED_MODULE_3__["parallelizeBatches"])(batchedProjects, async pkg => { + const completionHint = await Object(_utils_watch__WEBPACK_IMPORTED_MODULE_5__["waitUntilWatchIsReady"])(pkg.runScriptStreaming(watchScriptName, { debug: false }).stdout // TypeScript note: As long as the proc stdio[1] is 'pipe', then stdout will not be null ); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`[${pkg.name}] Initial build completed (${completionHint}).`); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].success(`[${pkg.name}] Initial build completed (${completionHint}).`); }); } }; /***/ }), -/* 513 */ +/* 514 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59567,19 +59636,52 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 514 */ +/* 515 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "WatchBazelCommand", function() { return WatchBazelCommand; }); +/* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(372); +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const WatchBazelCommand = { + description: 'Runs a build in the Bazel built packages and keeps watching them for changes', + name: 'watch-bazel', + + async run(projects, projectGraph, { + options + }) { + const runOffline = (options === null || options === void 0 ? void 0 : options.offline) === true; // Call bazel with the target to build all available packages and run it through iBazel to watch it for changes + // + // Note: --run_output=false arg will disable the iBazel notifications about gazelle and buildozer when running it + // Can also be solved by adding a root `.bazel_fix_commands.json` but its not needed at the moment + + await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_0__["runIBazel"])(['--run_output=false', 'build', '//packages:build'], runOffline); + } + +}; + +/***/ }), +/* 516 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); -/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(515); +/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(517); /* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(249); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(246); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); /* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(371); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(558); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(560); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59697,7 +59799,7 @@ function toArray(value) { } /***/ }), -/* 515 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59716,8 +59818,8 @@ const util_1 = __webpack_require__(112); const os_1 = tslib_1.__importDefault(__webpack_require__(121)); const fs_1 = tslib_1.__importDefault(__webpack_require__(134)); const path_1 = tslib_1.__importDefault(__webpack_require__(4)); -const axios_1 = tslib_1.__importDefault(__webpack_require__(516)); -const ci_stats_config_1 = __webpack_require__(556); +const axios_1 = tslib_1.__importDefault(__webpack_require__(518)); +const ci_stats_config_1 = __webpack_require__(558); const BASE_URL = 'https://ci-stats.kibana.dev'; class CiStatsReporter { constructor(config, log) { @@ -59805,7 +59907,7 @@ class CiStatsReporter { // specify the module id in a way that will keep webpack from bundling extra code into @kbn/pm const hideFromWebpack = ['@', 'kbn/utils']; // eslint-disable-next-line @typescript-eslint/no-var-requires - const { kibanaPackageJson } = __webpack_require__(557)(hideFromWebpack.join('')); + const { kibanaPackageJson } = __webpack_require__(559)(hideFromWebpack.join('')); return kibanaPackageJson.branch; } /** @@ -59817,7 +59919,7 @@ class CiStatsReporter { // specify the module id in a way that will keep webpack from bundling extra code into @kbn/pm const hideFromWebpack = ['@', 'kbn/utils']; // eslint-disable-next-line @typescript-eslint/no-var-requires - const { REPO_ROOT } = __webpack_require__(557)(hideFromWebpack.join('')); + const { REPO_ROOT } = __webpack_require__(559)(hideFromWebpack.join('')); try { return fs_1.default.readFileSync(path_1.default.resolve(REPO_ROOT, 'data/uuid'), 'utf-8').trim(); } @@ -59880,23 +59982,23 @@ exports.CiStatsReporter = CiStatsReporter; //# sourceMappingURL=ci_stats_reporter.js.map /***/ }), -/* 516 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = __webpack_require__(517); +module.exports = __webpack_require__(519); /***/ }), -/* 517 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); -var bind = __webpack_require__(519); -var Axios = __webpack_require__(520); -var mergeConfig = __webpack_require__(551); -var defaults = __webpack_require__(526); +var utils = __webpack_require__(520); +var bind = __webpack_require__(521); +var Axios = __webpack_require__(522); +var mergeConfig = __webpack_require__(553); +var defaults = __webpack_require__(528); /** * Create an instance of Axios @@ -59929,18 +60031,18 @@ axios.create = function create(instanceConfig) { }; // Expose Cancel & CancelToken -axios.Cancel = __webpack_require__(552); -axios.CancelToken = __webpack_require__(553); -axios.isCancel = __webpack_require__(525); +axios.Cancel = __webpack_require__(554); +axios.CancelToken = __webpack_require__(555); +axios.isCancel = __webpack_require__(527); // Expose all/spread axios.all = function all(promises) { return Promise.all(promises); }; -axios.spread = __webpack_require__(554); +axios.spread = __webpack_require__(556); // Expose isAxiosError -axios.isAxiosError = __webpack_require__(555); +axios.isAxiosError = __webpack_require__(557); module.exports = axios; @@ -59949,13 +60051,13 @@ module.exports.default = axios; /***/ }), -/* 518 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var bind = __webpack_require__(519); +var bind = __webpack_require__(521); /*global toString:true*/ @@ -60307,7 +60409,7 @@ module.exports = { /***/ }), -/* 519 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60325,17 +60427,17 @@ module.exports = function bind(fn, thisArg) { /***/ }), -/* 520 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); -var buildURL = __webpack_require__(521); -var InterceptorManager = __webpack_require__(522); -var dispatchRequest = __webpack_require__(523); -var mergeConfig = __webpack_require__(551); +var utils = __webpack_require__(520); +var buildURL = __webpack_require__(523); +var InterceptorManager = __webpack_require__(524); +var dispatchRequest = __webpack_require__(525); +var mergeConfig = __webpack_require__(553); /** * Create a new instance of Axios @@ -60427,13 +60529,13 @@ module.exports = Axios; /***/ }), -/* 521 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); function encode(val) { return encodeURIComponent(val). @@ -60504,13 +60606,13 @@ module.exports = function buildURL(url, params, paramsSerializer) { /***/ }), -/* 522 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); function InterceptorManager() { this.handlers = []; @@ -60563,16 +60665,16 @@ module.exports = InterceptorManager; /***/ }), -/* 523 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); -var transformData = __webpack_require__(524); -var isCancel = __webpack_require__(525); -var defaults = __webpack_require__(526); +var utils = __webpack_require__(520); +var transformData = __webpack_require__(526); +var isCancel = __webpack_require__(527); +var defaults = __webpack_require__(528); /** * Throws a `Cancel` if cancellation has been requested. @@ -60649,13 +60751,13 @@ module.exports = function dispatchRequest(config) { /***/ }), -/* 524 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); /** * Transform the data for a request or a response @@ -60676,7 +60778,7 @@ module.exports = function transformData(data, headers, fns) { /***/ }), -/* 525 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60688,14 +60790,14 @@ module.exports = function isCancel(value) { /***/ }), -/* 526 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); -var normalizeHeaderName = __webpack_require__(527); +var utils = __webpack_require__(520); +var normalizeHeaderName = __webpack_require__(529); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -60711,10 +60813,10 @@ function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter - adapter = __webpack_require__(528); + adapter = __webpack_require__(530); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // For node use HTTP adapter - adapter = __webpack_require__(538); + adapter = __webpack_require__(540); } return adapter; } @@ -60793,13 +60895,13 @@ module.exports = defaults; /***/ }), -/* 527 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); module.exports = function normalizeHeaderName(headers, normalizedName) { utils.forEach(headers, function processHeader(value, name) { @@ -60812,20 +60914,20 @@ module.exports = function normalizeHeaderName(headers, normalizedName) { /***/ }), -/* 528 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); -var settle = __webpack_require__(529); -var cookies = __webpack_require__(532); -var buildURL = __webpack_require__(521); -var buildFullPath = __webpack_require__(533); -var parseHeaders = __webpack_require__(536); -var isURLSameOrigin = __webpack_require__(537); -var createError = __webpack_require__(530); +var utils = __webpack_require__(520); +var settle = __webpack_require__(531); +var cookies = __webpack_require__(534); +var buildURL = __webpack_require__(523); +var buildFullPath = __webpack_require__(535); +var parseHeaders = __webpack_require__(538); +var isURLSameOrigin = __webpack_require__(539); +var createError = __webpack_require__(532); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { @@ -60998,13 +61100,13 @@ module.exports = function xhrAdapter(config) { /***/ }), -/* 529 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var createError = __webpack_require__(530); +var createError = __webpack_require__(532); /** * Resolve or reject a Promise based on response status. @@ -61030,13 +61132,13 @@ module.exports = function settle(resolve, reject, response) { /***/ }), -/* 530 */ +/* 532 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var enhanceError = __webpack_require__(531); +var enhanceError = __webpack_require__(533); /** * Create an Error with the specified message, config, error code, request and response. @@ -61055,7 +61157,7 @@ module.exports = function createError(message, config, code, request, response) /***/ }), -/* 531 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61104,13 +61206,13 @@ module.exports = function enhanceError(error, config, code, request, response) { /***/ }), -/* 532 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); module.exports = ( utils.isStandardBrowserEnv() ? @@ -61164,14 +61266,14 @@ module.exports = ( /***/ }), -/* 533 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isAbsoluteURL = __webpack_require__(534); -var combineURLs = __webpack_require__(535); +var isAbsoluteURL = __webpack_require__(536); +var combineURLs = __webpack_require__(537); /** * Creates a new URL by combining the baseURL with the requestedURL, @@ -61191,7 +61293,7 @@ module.exports = function buildFullPath(baseURL, requestedURL) { /***/ }), -/* 534 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61212,7 +61314,7 @@ module.exports = function isAbsoluteURL(url) { /***/ }), -/* 535 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61233,13 +61335,13 @@ module.exports = function combineURLs(baseURL, relativeURL) { /***/ }), -/* 536 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); // Headers whose duplicates are ignored by node // c.f. https://nodejs.org/api/http.html#http_message_headers @@ -61293,13 +61395,13 @@ module.exports = function parseHeaders(headers) { /***/ }), -/* 537 */ +/* 539 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); module.exports = ( utils.isStandardBrowserEnv() ? @@ -61368,25 +61470,25 @@ module.exports = ( /***/ }), -/* 538 */ +/* 540 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); -var settle = __webpack_require__(529); -var buildFullPath = __webpack_require__(533); -var buildURL = __webpack_require__(521); -var http = __webpack_require__(539); -var https = __webpack_require__(540); -var httpFollow = __webpack_require__(541).http; -var httpsFollow = __webpack_require__(541).https; +var utils = __webpack_require__(520); +var settle = __webpack_require__(531); +var buildFullPath = __webpack_require__(535); +var buildURL = __webpack_require__(523); +var http = __webpack_require__(541); +var https = __webpack_require__(542); +var httpFollow = __webpack_require__(543).http; +var httpsFollow = __webpack_require__(543).https; var url = __webpack_require__(283); -var zlib = __webpack_require__(549); -var pkg = __webpack_require__(550); -var createError = __webpack_require__(530); -var enhanceError = __webpack_require__(531); +var zlib = __webpack_require__(551); +var pkg = __webpack_require__(552); +var createError = __webpack_require__(532); +var enhanceError = __webpack_require__(533); var isHttps = /https:?/; @@ -61678,28 +61780,28 @@ module.exports = function httpAdapter(config) { /***/ }), -/* 539 */ +/* 541 */ /***/ (function(module, exports) { module.exports = require("http"); /***/ }), -/* 540 */ +/* 542 */ /***/ (function(module, exports) { module.exports = require("https"); /***/ }), -/* 541 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { var url = __webpack_require__(283); var URL = url.URL; -var http = __webpack_require__(539); -var https = __webpack_require__(540); +var http = __webpack_require__(541); +var https = __webpack_require__(542); var Writable = __webpack_require__(138).Writable; var assert = __webpack_require__(140); -var debug = __webpack_require__(542); +var debug = __webpack_require__(544); // Create handlers that pass events from native requests var eventHandlers = Object.create(null); @@ -62194,13 +62296,13 @@ module.exports.wrap = wrap; /***/ }), -/* 542 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { var debug; try { /* eslint global-require: off */ - debug = __webpack_require__(543)("follow-redirects"); + debug = __webpack_require__(545)("follow-redirects"); } catch (error) { debug = function () { /* */ }; @@ -62209,7 +62311,7 @@ module.exports = debug; /***/ }), -/* 543 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -62218,14 +62320,14 @@ module.exports = debug; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(544); + module.exports = __webpack_require__(546); } else { - module.exports = __webpack_require__(547); + module.exports = __webpack_require__(549); } /***/ }), -/* 544 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -62234,7 +62336,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(545); +exports = module.exports = __webpack_require__(547); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -62416,7 +62518,7 @@ function localstorage() { /***/ }), -/* 545 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { @@ -62432,7 +62534,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(546); +exports.humanize = __webpack_require__(548); /** * The currently active debug mode names, and names to skip. @@ -62624,7 +62726,7 @@ function coerce(val) { /***/ }), -/* 546 */ +/* 548 */ /***/ (function(module, exports) { /** @@ -62782,7 +62884,7 @@ function plural(ms, n, name) { /***/ }), -/* 547 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -62798,7 +62900,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(545); +exports = module.exports = __webpack_require__(547); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -62977,7 +63079,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(548); + var net = __webpack_require__(550); stream = new net.Socket({ fd: fd, readable: false, @@ -63036,31 +63138,31 @@ exports.enable(load()); /***/ }), -/* 548 */ +/* 550 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 549 */ +/* 551 */ /***/ (function(module, exports) { module.exports = require("zlib"); /***/ }), -/* 550 */ +/* 552 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"axios\",\"version\":\"0.21.1\",\"description\":\"Promise based HTTP client for the browser and node.js\",\"main\":\"index.js\",\"scripts\":{\"test\":\"grunt test && bundlesize\",\"start\":\"node ./sandbox/server.js\",\"build\":\"NODE_ENV=production grunt build\",\"preversion\":\"npm test\",\"version\":\"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json\",\"postversion\":\"git push && git push --tags\",\"examples\":\"node ./examples/server.js\",\"coveralls\":\"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js\",\"fix\":\"eslint --fix lib/**/*.js\"},\"repository\":{\"type\":\"git\",\"url\":\"https://github.com/axios/axios.git\"},\"keywords\":[\"xhr\",\"http\",\"ajax\",\"promise\",\"node\"],\"author\":\"Matt Zabriskie\",\"license\":\"MIT\",\"bugs\":{\"url\":\"https://github.com/axios/axios/issues\"},\"homepage\":\"https://github.com/axios/axios\",\"devDependencies\":{\"bundlesize\":\"^0.17.0\",\"coveralls\":\"^3.0.0\",\"es6-promise\":\"^4.2.4\",\"grunt\":\"^1.0.2\",\"grunt-banner\":\"^0.6.0\",\"grunt-cli\":\"^1.2.0\",\"grunt-contrib-clean\":\"^1.1.0\",\"grunt-contrib-watch\":\"^1.0.0\",\"grunt-eslint\":\"^20.1.0\",\"grunt-karma\":\"^2.0.0\",\"grunt-mocha-test\":\"^0.13.3\",\"grunt-ts\":\"^6.0.0-beta.19\",\"grunt-webpack\":\"^1.0.18\",\"istanbul-instrumenter-loader\":\"^1.0.0\",\"jasmine-core\":\"^2.4.1\",\"karma\":\"^1.3.0\",\"karma-chrome-launcher\":\"^2.2.0\",\"karma-coverage\":\"^1.1.1\",\"karma-firefox-launcher\":\"^1.1.0\",\"karma-jasmine\":\"^1.1.1\",\"karma-jasmine-ajax\":\"^0.1.13\",\"karma-opera-launcher\":\"^1.0.0\",\"karma-safari-launcher\":\"^1.0.0\",\"karma-sauce-launcher\":\"^1.2.0\",\"karma-sinon\":\"^1.0.5\",\"karma-sourcemap-loader\":\"^0.3.7\",\"karma-webpack\":\"^1.7.0\",\"load-grunt-tasks\":\"^3.5.2\",\"minimist\":\"^1.2.0\",\"mocha\":\"^5.2.0\",\"sinon\":\"^4.5.0\",\"typescript\":\"^2.8.1\",\"url-search-params\":\"^0.10.0\",\"webpack\":\"^1.13.1\",\"webpack-dev-server\":\"^1.14.1\"},\"browser\":{\"./lib/adapters/http.js\":\"./lib/adapters/xhr.js\"},\"jsdelivr\":\"dist/axios.min.js\",\"unpkg\":\"dist/axios.min.js\",\"typings\":\"./index.d.ts\",\"dependencies\":{\"follow-redirects\":\"^1.10.0\"},\"bundlesize\":[{\"path\":\"./dist/axios.min.js\",\"threshold\":\"5kB\"}]}"); /***/ }), -/* 551 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(518); +var utils = __webpack_require__(520); /** * Config-specific merge-function which creates a new config-object @@ -63148,7 +63250,7 @@ module.exports = function mergeConfig(config1, config2) { /***/ }), -/* 552 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63174,13 +63276,13 @@ module.exports = Cancel; /***/ }), -/* 553 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Cancel = __webpack_require__(552); +var Cancel = __webpack_require__(554); /** * A `CancelToken` is an object that can be used to request cancellation of an operation. @@ -63238,7 +63340,7 @@ module.exports = CancelToken; /***/ }), -/* 554 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63272,7 +63374,7 @@ module.exports = function spread(callback) { /***/ }), -/* 555 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63290,7 +63392,7 @@ module.exports = function isAxiosError(payload) { /***/ }), -/* 556 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63340,7 +63442,7 @@ exports.parseConfig = parseConfig; //# sourceMappingURL=ci_stats_config.js.map /***/ }), -/* 557 */ +/* 559 */ /***/ (function(module, exports) { function webpackEmptyContext(req) { @@ -63351,10 +63453,10 @@ function webpackEmptyContext(req) { webpackEmptyContext.keys = function() { return []; }; webpackEmptyContext.resolve = webpackEmptyContext; module.exports = webpackEmptyContext; -webpackEmptyContext.id = 557; +webpackEmptyContext.id = 559; /***/ }), -/* 558 */ +/* 560 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -63364,13 +63466,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(134); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(559); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(561); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(239); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(366); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(248); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(562); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(564); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -63534,15 +63636,15 @@ class Kibana { } /***/ }), -/* 559 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(150); const arrayUnion = __webpack_require__(145); -const arrayDiffer = __webpack_require__(560); -const arrify = __webpack_require__(561); +const arrayDiffer = __webpack_require__(562); +const arrify = __webpack_require__(563); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -63566,7 +63668,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 560 */ +/* 562 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63581,7 +63683,7 @@ module.exports = arrayDiffer; /***/ }), -/* 561 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63611,7 +63713,7 @@ module.exports = arrify; /***/ }), -/* 562 */ +/* 564 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -63670,15 +63772,15 @@ function getProjectPaths({ } /***/ }), -/* 563 */ +/* 565 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); +/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(566); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(812); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(814); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -63692,19 +63794,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 564 */ +/* 566 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(567); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(774); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(776); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(812); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(814); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(372); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); @@ -63799,7 +63901,7 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { } /***/ }), -/* 565 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63807,14 +63909,14 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(156); const path = __webpack_require__(4); const os = __webpack_require__(121); -const pMap = __webpack_require__(566); -const arrify = __webpack_require__(561); -const globby = __webpack_require__(569); -const hasGlob = __webpack_require__(758); -const cpFile = __webpack_require__(760); -const junk = __webpack_require__(770); -const pFilter = __webpack_require__(771); -const CpyError = __webpack_require__(773); +const pMap = __webpack_require__(568); +const arrify = __webpack_require__(563); +const globby = __webpack_require__(571); +const hasGlob = __webpack_require__(760); +const cpFile = __webpack_require__(762); +const junk = __webpack_require__(772); +const pFilter = __webpack_require__(773); +const CpyError = __webpack_require__(775); const defaultOptions = { ignoreJunk: true @@ -63965,12 +64067,12 @@ module.exports = (source, destination, { /***/ }), -/* 566 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(567); +const AggregateError = __webpack_require__(569); module.exports = async ( iterable, @@ -64053,12 +64155,12 @@ module.exports = async ( /***/ }), -/* 567 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(568); +const indentString = __webpack_require__(570); const cleanStack = __webpack_require__(244); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -64107,7 +64209,7 @@ module.exports = AggregateError; /***/ }), -/* 568 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64149,17 +64251,17 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 569 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(570); +const arrayUnion = __webpack_require__(572); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(572); -const dirGlob = __webpack_require__(751); -const gitignore = __webpack_require__(754); +const fastGlob = __webpack_require__(574); +const dirGlob = __webpack_require__(753); +const gitignore = __webpack_require__(756); const DEFAULT_FILTER = () => false; @@ -64304,12 +64406,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 570 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(571); +var arrayUniq = __webpack_require__(573); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -64317,7 +64419,7 @@ module.exports = function () { /***/ }), -/* 571 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64386,10 +64488,10 @@ if ('Set' in global) { /***/ }), -/* 572 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(573); +const pkg = __webpack_require__(575); module.exports = pkg.async; module.exports.default = pkg.async; @@ -64402,19 +64504,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 573 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(574); -var taskManager = __webpack_require__(575); -var reader_async_1 = __webpack_require__(722); -var reader_stream_1 = __webpack_require__(746); -var reader_sync_1 = __webpack_require__(747); -var arrayUtils = __webpack_require__(749); -var streamUtils = __webpack_require__(750); +var optionsManager = __webpack_require__(576); +var taskManager = __webpack_require__(577); +var reader_async_1 = __webpack_require__(724); +var reader_stream_1 = __webpack_require__(748); +var reader_sync_1 = __webpack_require__(749); +var arrayUtils = __webpack_require__(751); +var streamUtils = __webpack_require__(752); /** * Synchronous API. */ @@ -64480,7 +64582,7 @@ function isString(source) { /***/ }), -/* 574 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64518,13 +64620,13 @@ exports.prepare = prepare; /***/ }), -/* 575 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(576); +var patternUtils = __webpack_require__(578); /** * Generate tasks based on parent directory of each pattern. */ @@ -64615,16 +64717,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 576 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(577); +var globParent = __webpack_require__(579); var isGlob = __webpack_require__(172); -var micromatch = __webpack_require__(580); +var micromatch = __webpack_require__(582); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -64770,15 +64872,15 @@ exports.matchAny = matchAny; /***/ }), -/* 577 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(578); -var pathDirname = __webpack_require__(579); +var isglob = __webpack_require__(580); +var pathDirname = __webpack_require__(581); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -64801,7 +64903,7 @@ module.exports = function globParent(str) { /***/ }), -/* 578 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -64832,7 +64934,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 579 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64982,7 +65084,7 @@ module.exports.win32 = win32; /***/ }), -/* 580 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64993,18 +65095,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(112); -var braces = __webpack_require__(581); -var toRegex = __webpack_require__(582); -var extend = __webpack_require__(690); +var braces = __webpack_require__(583); +var toRegex = __webpack_require__(584); +var extend = __webpack_require__(692); /** * Local dependencies */ -var compilers = __webpack_require__(692); -var parsers = __webpack_require__(718); -var cache = __webpack_require__(719); -var utils = __webpack_require__(720); +var compilers = __webpack_require__(694); +var parsers = __webpack_require__(720); +var cache = __webpack_require__(721); +var utils = __webpack_require__(722); var MAX_LENGTH = 1024 * 64; /** @@ -65866,7 +65968,7 @@ module.exports = micromatch; /***/ }), -/* 581 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65876,18 +65978,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(582); -var unique = __webpack_require__(602); -var extend = __webpack_require__(603); +var toRegex = __webpack_require__(584); +var unique = __webpack_require__(604); +var extend = __webpack_require__(605); /** * Local dependencies */ -var compilers = __webpack_require__(605); -var parsers = __webpack_require__(618); -var Braces = __webpack_require__(623); -var utils = __webpack_require__(606); +var compilers = __webpack_require__(607); +var parsers = __webpack_require__(620); +var Braces = __webpack_require__(625); +var utils = __webpack_require__(608); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -66191,16 +66293,16 @@ module.exports = braces; /***/ }), -/* 582 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(583); -var define = __webpack_require__(589); -var extend = __webpack_require__(595); -var not = __webpack_require__(599); +var safe = __webpack_require__(585); +var define = __webpack_require__(591); +var extend = __webpack_require__(597); +var not = __webpack_require__(601); var MAX_LENGTH = 1024 * 64; /** @@ -66353,10 +66455,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 583 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(584); +var parse = __webpack_require__(586); var types = parse.types; module.exports = function (re, opts) { @@ -66402,13 +66504,13 @@ function isRegExp (x) { /***/ }), -/* 584 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(585); -var types = __webpack_require__(586); -var sets = __webpack_require__(587); -var positions = __webpack_require__(588); +var util = __webpack_require__(587); +var types = __webpack_require__(588); +var sets = __webpack_require__(589); +var positions = __webpack_require__(590); module.exports = function(regexpStr) { @@ -66690,11 +66792,11 @@ module.exports.types = types; /***/ }), -/* 585 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(586); -var sets = __webpack_require__(587); +var types = __webpack_require__(588); +var sets = __webpack_require__(589); // All of these are private and only used by randexp. @@ -66807,7 +66909,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 586 */ +/* 588 */ /***/ (function(module, exports) { module.exports = { @@ -66823,10 +66925,10 @@ module.exports = { /***/ }), -/* 587 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(586); +var types = __webpack_require__(588); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -66911,10 +67013,10 @@ exports.anyChar = function() { /***/ }), -/* 588 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(586); +var types = __webpack_require__(588); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -66934,7 +67036,7 @@ exports.end = function() { /***/ }), -/* 589 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66947,8 +67049,8 @@ exports.end = function() { -var isobject = __webpack_require__(590); -var isDescriptor = __webpack_require__(591); +var isobject = __webpack_require__(592); +var isDescriptor = __webpack_require__(593); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -66979,7 +67081,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 590 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66998,7 +67100,7 @@ module.exports = function isObject(val) { /***/ }), -/* 591 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67011,9 +67113,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(592); -var isAccessor = __webpack_require__(593); -var isData = __webpack_require__(594); +var typeOf = __webpack_require__(594); +var isAccessor = __webpack_require__(595); +var isData = __webpack_require__(596); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -67027,7 +67129,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 592 */ +/* 594 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -67162,7 +67264,7 @@ function isBuffer(val) { /***/ }), -/* 593 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67175,7 +67277,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(592); +var typeOf = __webpack_require__(594); // accessor descriptor properties var accessor = { @@ -67238,7 +67340,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 594 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67251,7 +67353,7 @@ module.exports = isAccessorDescriptor; -var typeOf = __webpack_require__(592); +var typeOf = __webpack_require__(594); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -67294,14 +67396,14 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 595 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(596); -var assignSymbols = __webpack_require__(598); +var isExtendable = __webpack_require__(598); +var assignSymbols = __webpack_require__(600); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67361,7 +67463,7 @@ function isEnum(obj, key) { /***/ }), -/* 596 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67374,7 +67476,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(597); +var isPlainObject = __webpack_require__(599); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67382,7 +67484,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 597 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67395,7 +67497,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(590); +var isObject = __webpack_require__(592); function isObjectObject(o) { return isObject(o) === true @@ -67426,7 +67528,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 598 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67473,14 +67575,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 599 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(600); -var safe = __webpack_require__(583); +var extend = __webpack_require__(602); +var safe = __webpack_require__(585); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -67552,14 +67654,14 @@ module.exports = toRegex; /***/ }), -/* 600 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(601); -var assignSymbols = __webpack_require__(598); +var isExtendable = __webpack_require__(603); +var assignSymbols = __webpack_require__(600); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67619,7 +67721,7 @@ function isEnum(obj, key) { /***/ }), -/* 601 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67632,7 +67734,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(597); +var isPlainObject = __webpack_require__(599); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67640,7 +67742,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 602 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67690,13 +67792,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 603 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(604); +var isObject = __webpack_require__(606); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -67730,7 +67832,7 @@ function hasOwn(obj, key) { /***/ }), -/* 604 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67750,13 +67852,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 605 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(606); +var utils = __webpack_require__(608); module.exports = function(braces, options) { braces.compiler @@ -68039,25 +68141,25 @@ function hasQueue(node) { /***/ }), -/* 606 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(607); +var splitString = __webpack_require__(609); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(603); -utils.flatten = __webpack_require__(610); -utils.isObject = __webpack_require__(590); -utils.fillRange = __webpack_require__(611); -utils.repeat = __webpack_require__(617); -utils.unique = __webpack_require__(602); +utils.extend = __webpack_require__(605); +utils.flatten = __webpack_require__(612); +utils.isObject = __webpack_require__(592); +utils.fillRange = __webpack_require__(613); +utils.repeat = __webpack_require__(619); +utils.unique = __webpack_require__(604); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -68389,7 +68491,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 607 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68402,7 +68504,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(608); +var extend = __webpack_require__(610); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -68567,14 +68669,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 608 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(609); -var assignSymbols = __webpack_require__(598); +var isExtendable = __webpack_require__(611); +var assignSymbols = __webpack_require__(600); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -68634,7 +68736,7 @@ function isEnum(obj, key) { /***/ }), -/* 609 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68647,7 +68749,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(597); +var isPlainObject = __webpack_require__(599); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -68655,7 +68757,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 610 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68684,7 +68786,7 @@ function flat(arr, res) { /***/ }), -/* 611 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68698,10 +68800,10 @@ function flat(arr, res) { var util = __webpack_require__(112); -var isNumber = __webpack_require__(612); -var extend = __webpack_require__(603); -var repeat = __webpack_require__(615); -var toRegex = __webpack_require__(616); +var isNumber = __webpack_require__(614); +var extend = __webpack_require__(605); +var repeat = __webpack_require__(617); +var toRegex = __webpack_require__(618); /** * Return a range of numbers or letters. @@ -68899,7 +69001,7 @@ module.exports = fillRange; /***/ }), -/* 612 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68912,7 +69014,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(613); +var typeOf = __webpack_require__(615); module.exports = function isNumber(num) { var type = typeOf(num); @@ -68928,10 +69030,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 613 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(614); +var isBuffer = __webpack_require__(616); var toString = Object.prototype.toString; /** @@ -69050,7 +69152,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 614 */ +/* 616 */ /***/ (function(module, exports) { /*! @@ -69077,7 +69179,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 615 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69154,7 +69256,7 @@ function repeat(str, num) { /***/ }), -/* 616 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69167,8 +69269,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(615); -var isNumber = __webpack_require__(612); +var repeat = __webpack_require__(617); +var isNumber = __webpack_require__(614); var cache = {}; function toRegexRange(min, max, options) { @@ -69455,7 +69557,7 @@ module.exports = toRegexRange; /***/ }), -/* 617 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69480,14 +69582,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 618 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(619); -var utils = __webpack_require__(606); +var Node = __webpack_require__(621); +var utils = __webpack_require__(608); /** * Braces parsers @@ -69847,15 +69949,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 619 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(590); -var define = __webpack_require__(620); -var utils = __webpack_require__(621); +var isObject = __webpack_require__(592); +var define = __webpack_require__(622); +var utils = __webpack_require__(623); var ownNames; /** @@ -70346,7 +70448,7 @@ exports = module.exports = Node; /***/ }), -/* 620 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70359,7 +70461,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(591); +var isDescriptor = __webpack_require__(593); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70384,13 +70486,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 621 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(622); +var typeOf = __webpack_require__(624); var utils = module.exports; /** @@ -71410,10 +71512,10 @@ function assert(val, message) { /***/ }), -/* 622 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(614); +var isBuffer = __webpack_require__(616); var toString = Object.prototype.toString; /** @@ -71532,17 +71634,17 @@ module.exports = function kindOf(val) { /***/ }), -/* 623 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(603); -var Snapdragon = __webpack_require__(624); -var compilers = __webpack_require__(605); -var parsers = __webpack_require__(618); -var utils = __webpack_require__(606); +var extend = __webpack_require__(605); +var Snapdragon = __webpack_require__(626); +var compilers = __webpack_require__(607); +var parsers = __webpack_require__(620); +var utils = __webpack_require__(608); /** * Customize Snapdragon parser and renderer @@ -71643,17 +71745,17 @@ module.exports = Braces; /***/ }), -/* 624 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(625); -var define = __webpack_require__(653); -var Compiler = __webpack_require__(664); -var Parser = __webpack_require__(687); -var utils = __webpack_require__(667); +var Base = __webpack_require__(627); +var define = __webpack_require__(655); +var Compiler = __webpack_require__(666); +var Parser = __webpack_require__(689); +var utils = __webpack_require__(669); var regexCache = {}; var cache = {}; @@ -71824,20 +71926,20 @@ module.exports.Parser = Parser; /***/ }), -/* 625 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(626); -var CacheBase = __webpack_require__(627); -var Emitter = __webpack_require__(628); -var isObject = __webpack_require__(590); -var merge = __webpack_require__(647); -var pascal = __webpack_require__(650); -var cu = __webpack_require__(651); +var define = __webpack_require__(628); +var CacheBase = __webpack_require__(629); +var Emitter = __webpack_require__(630); +var isObject = __webpack_require__(592); +var merge = __webpack_require__(649); +var pascal = __webpack_require__(652); +var cu = __webpack_require__(653); /** * Optionally define a custom `cache` namespace to use. @@ -72266,7 +72368,7 @@ module.exports.namespace = namespace; /***/ }), -/* 626 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72279,7 +72381,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(591); +var isDescriptor = __webpack_require__(593); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -72304,21 +72406,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 627 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(590); -var Emitter = __webpack_require__(628); -var visit = __webpack_require__(629); -var toPath = __webpack_require__(632); -var union = __webpack_require__(634); -var del = __webpack_require__(638); -var get = __webpack_require__(636); -var has = __webpack_require__(643); -var set = __webpack_require__(646); +var isObject = __webpack_require__(592); +var Emitter = __webpack_require__(630); +var visit = __webpack_require__(631); +var toPath = __webpack_require__(634); +var union = __webpack_require__(636); +var del = __webpack_require__(640); +var get = __webpack_require__(638); +var has = __webpack_require__(645); +var set = __webpack_require__(648); /** * Create a `Cache` constructor that when instantiated will @@ -72572,7 +72674,7 @@ module.exports.namespace = namespace; /***/ }), -/* 628 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { @@ -72741,7 +72843,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 629 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72754,8 +72856,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(630); -var mapVisit = __webpack_require__(631); +var visit = __webpack_require__(632); +var mapVisit = __webpack_require__(633); module.exports = function(collection, method, val) { var result; @@ -72778,7 +72880,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 630 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72791,7 +72893,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(590); +var isObject = __webpack_require__(592); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -72818,14 +72920,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 631 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(630); +var visit = __webpack_require__(632); /** * Map `visit` over an array of objects. @@ -72862,7 +72964,7 @@ function isObject(val) { /***/ }), -/* 632 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72875,7 +72977,7 @@ function isObject(val) { -var typeOf = __webpack_require__(633); +var typeOf = __webpack_require__(635); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -72902,10 +73004,10 @@ function filter(arr) { /***/ }), -/* 633 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(614); +var isBuffer = __webpack_require__(616); var toString = Object.prototype.toString; /** @@ -73024,16 +73126,16 @@ module.exports = function kindOf(val) { /***/ }), -/* 634 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(604); -var union = __webpack_require__(635); -var get = __webpack_require__(636); -var set = __webpack_require__(637); +var isObject = __webpack_require__(606); +var union = __webpack_require__(637); +var get = __webpack_require__(638); +var set = __webpack_require__(639); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -73061,7 +73163,7 @@ function arrayify(val) { /***/ }), -/* 635 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73097,7 +73199,7 @@ module.exports = function union(init) { /***/ }), -/* 636 */ +/* 638 */ /***/ (function(module, exports) { /*! @@ -73153,7 +73255,7 @@ function toString(val) { /***/ }), -/* 637 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73166,10 +73268,10 @@ function toString(val) { -var split = __webpack_require__(607); -var extend = __webpack_require__(603); -var isPlainObject = __webpack_require__(597); -var isObject = __webpack_require__(604); +var split = __webpack_require__(609); +var extend = __webpack_require__(605); +var isPlainObject = __webpack_require__(599); +var isObject = __webpack_require__(606); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -73215,7 +73317,7 @@ function isValidKey(key) { /***/ }), -/* 638 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73228,8 +73330,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(590); -var has = __webpack_require__(639); +var isObject = __webpack_require__(592); +var has = __webpack_require__(641); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -73254,7 +73356,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 639 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73267,9 +73369,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(640); -var hasValues = __webpack_require__(642); -var get = __webpack_require__(636); +var isObject = __webpack_require__(642); +var hasValues = __webpack_require__(644); +var get = __webpack_require__(638); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -73280,7 +73382,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 640 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73293,7 +73395,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(641); +var isArray = __webpack_require__(643); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -73301,7 +73403,7 @@ module.exports = function isObject(val) { /***/ }), -/* 641 */ +/* 643 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -73312,7 +73414,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 642 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73355,7 +73457,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 643 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73368,9 +73470,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(590); -var hasValues = __webpack_require__(644); -var get = __webpack_require__(636); +var isObject = __webpack_require__(592); +var hasValues = __webpack_require__(646); +var get = __webpack_require__(638); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -73378,7 +73480,7 @@ module.exports = function(val, prop) { /***/ }), -/* 644 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73391,8 +73493,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(645); -var isNumber = __webpack_require__(612); +var typeOf = __webpack_require__(647); +var isNumber = __webpack_require__(614); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -73445,10 +73547,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 645 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(614); +var isBuffer = __webpack_require__(616); var toString = Object.prototype.toString; /** @@ -73570,7 +73672,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 646 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73583,10 +73685,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(607); -var extend = __webpack_require__(603); -var isPlainObject = __webpack_require__(597); -var isObject = __webpack_require__(604); +var split = __webpack_require__(609); +var extend = __webpack_require__(605); +var isPlainObject = __webpack_require__(599); +var isObject = __webpack_require__(606); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -73632,14 +73734,14 @@ function isValidKey(key) { /***/ }), -/* 647 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(648); -var forIn = __webpack_require__(649); +var isExtendable = __webpack_require__(650); +var forIn = __webpack_require__(651); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -73703,7 +73805,7 @@ module.exports = mixinDeep; /***/ }), -/* 648 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73716,7 +73818,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(597); +var isPlainObject = __webpack_require__(599); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -73724,7 +73826,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 649 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73747,7 +73849,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 650 */ +/* 652 */ /***/ (function(module, exports) { /*! @@ -73774,14 +73876,14 @@ module.exports = pascalcase; /***/ }), -/* 651 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(652); +var utils = __webpack_require__(654); /** * Expose class utils @@ -74146,7 +74248,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 652 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74160,10 +74262,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(635); -utils.define = __webpack_require__(653); -utils.isObj = __webpack_require__(590); -utils.staticExtend = __webpack_require__(660); +utils.union = __webpack_require__(637); +utils.define = __webpack_require__(655); +utils.isObj = __webpack_require__(592); +utils.staticExtend = __webpack_require__(662); /** @@ -74174,7 +74276,7 @@ module.exports = utils; /***/ }), -/* 653 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74187,7 +74289,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(654); +var isDescriptor = __webpack_require__(656); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -74212,7 +74314,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 654 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74225,9 +74327,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(655); -var isAccessor = __webpack_require__(656); -var isData = __webpack_require__(658); +var typeOf = __webpack_require__(657); +var isAccessor = __webpack_require__(658); +var isData = __webpack_require__(660); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -74241,7 +74343,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 655 */ +/* 657 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -74394,7 +74496,7 @@ function isBuffer(val) { /***/ }), -/* 656 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74407,7 +74509,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(657); +var typeOf = __webpack_require__(659); // accessor descriptor properties var accessor = { @@ -74470,10 +74572,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 657 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(614); +var isBuffer = __webpack_require__(616); var toString = Object.prototype.toString; /** @@ -74592,7 +74694,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 658 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74605,7 +74707,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(659); +var typeOf = __webpack_require__(661); // data descriptor properties var data = { @@ -74654,10 +74756,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 659 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(614); +var isBuffer = __webpack_require__(616); var toString = Object.prototype.toString; /** @@ -74776,7 +74878,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 660 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74789,8 +74891,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(661); -var define = __webpack_require__(653); +var copy = __webpack_require__(663); +var define = __webpack_require__(655); var util = __webpack_require__(112); /** @@ -74873,15 +74975,15 @@ module.exports = extend; /***/ }), -/* 661 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(662); -var copyDescriptor = __webpack_require__(663); -var define = __webpack_require__(653); +var typeOf = __webpack_require__(664); +var copyDescriptor = __webpack_require__(665); +var define = __webpack_require__(655); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -75054,10 +75156,10 @@ module.exports.has = has; /***/ }), -/* 662 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(614); +var isBuffer = __webpack_require__(616); var toString = Object.prototype.toString; /** @@ -75176,7 +75278,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 663 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75264,16 +75366,16 @@ function isObject(val) { /***/ }), -/* 664 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(665); -var define = __webpack_require__(653); -var debug = __webpack_require__(543)('snapdragon:compiler'); -var utils = __webpack_require__(667); +var use = __webpack_require__(667); +var define = __webpack_require__(655); +var debug = __webpack_require__(545)('snapdragon:compiler'); +var utils = __webpack_require__(669); /** * Create a new `Compiler` with the given `options`. @@ -75427,7 +75529,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(686); + var sourcemaps = __webpack_require__(688); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -75448,7 +75550,7 @@ module.exports = Compiler; /***/ }), -/* 665 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75461,7 +75563,7 @@ module.exports = Compiler; -var utils = __webpack_require__(666); +var utils = __webpack_require__(668); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -75576,7 +75678,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 666 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75590,8 +75692,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(653); -utils.isObject = __webpack_require__(590); +utils.define = __webpack_require__(655); +utils.isObject = __webpack_require__(592); utils.isString = function(val) { @@ -75606,7 +75708,7 @@ module.exports = utils; /***/ }), -/* 667 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75616,9 +75718,9 @@ module.exports = utils; * Module dependencies */ -exports.extend = __webpack_require__(603); -exports.SourceMap = __webpack_require__(668); -exports.sourceMapResolve = __webpack_require__(679); +exports.extend = __webpack_require__(605); +exports.SourceMap = __webpack_require__(670); +exports.sourceMapResolve = __webpack_require__(681); /** * Convert backslash in the given string to forward slashes @@ -75661,7 +75763,7 @@ exports.last = function(arr, n) { /***/ }), -/* 668 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -75669,13 +75771,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(669).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(675).SourceMapConsumer; -exports.SourceNode = __webpack_require__(678).SourceNode; +exports.SourceMapGenerator = __webpack_require__(671).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(677).SourceMapConsumer; +exports.SourceNode = __webpack_require__(680).SourceNode; /***/ }), -/* 669 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75685,10 +75787,10 @@ exports.SourceNode = __webpack_require__(678).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(670); -var util = __webpack_require__(672); -var ArraySet = __webpack_require__(673).ArraySet; -var MappingList = __webpack_require__(674).MappingList; +var base64VLQ = __webpack_require__(672); +var util = __webpack_require__(674); +var ArraySet = __webpack_require__(675).ArraySet; +var MappingList = __webpack_require__(676).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -76097,7 +76199,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 670 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76137,7 +76239,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(671); +var base64 = __webpack_require__(673); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -76243,7 +76345,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 671 */ +/* 673 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76316,7 +76418,7 @@ exports.decode = function (charCode) { /***/ }), -/* 672 */ +/* 674 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76739,7 +76841,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 673 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76749,7 +76851,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(672); +var util = __webpack_require__(674); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -76866,7 +76968,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 674 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76876,7 +76978,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(672); +var util = __webpack_require__(674); /** * Determine whether mappingB is after mappingA with respect to generated @@ -76951,7 +77053,7 @@ exports.MappingList = MappingList; /***/ }), -/* 675 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76961,11 +77063,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(672); -var binarySearch = __webpack_require__(676); -var ArraySet = __webpack_require__(673).ArraySet; -var base64VLQ = __webpack_require__(670); -var quickSort = __webpack_require__(677).quickSort; +var util = __webpack_require__(674); +var binarySearch = __webpack_require__(678); +var ArraySet = __webpack_require__(675).ArraySet; +var base64VLQ = __webpack_require__(672); +var quickSort = __webpack_require__(679).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -78039,7 +78141,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 676 */ +/* 678 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78156,7 +78258,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 677 */ +/* 679 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78276,7 +78378,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 678 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78286,8 +78388,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(669).SourceMapGenerator; -var util = __webpack_require__(672); +var SourceMapGenerator = __webpack_require__(671).SourceMapGenerator; +var util = __webpack_require__(674); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -78695,17 +78797,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 679 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(680) -var resolveUrl = __webpack_require__(681) -var decodeUriComponent = __webpack_require__(682) -var urix = __webpack_require__(684) -var atob = __webpack_require__(685) +var sourceMappingURL = __webpack_require__(682) +var resolveUrl = __webpack_require__(683) +var decodeUriComponent = __webpack_require__(684) +var urix = __webpack_require__(686) +var atob = __webpack_require__(687) @@ -79003,7 +79105,7 @@ module.exports = { /***/ }), -/* 680 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -79066,7 +79168,7 @@ void (function(root, factory) { /***/ }), -/* 681 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -79084,13 +79186,13 @@ module.exports = resolveUrl /***/ }), -/* 682 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(683) +var decodeUriComponent = __webpack_require__(685) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -79101,7 +79203,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 683 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79202,7 +79304,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 684 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -79225,7 +79327,7 @@ module.exports = urix /***/ }), -/* 685 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79239,7 +79341,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 686 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79247,8 +79349,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(653); -var utils = __webpack_require__(667); +var define = __webpack_require__(655); +var utils = __webpack_require__(669); /** * Expose `mixin()`. @@ -79391,19 +79493,19 @@ exports.comment = function(node) { /***/ }), -/* 687 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(665); +var use = __webpack_require__(667); var util = __webpack_require__(112); -var Cache = __webpack_require__(688); -var define = __webpack_require__(653); -var debug = __webpack_require__(543)('snapdragon:parser'); -var Position = __webpack_require__(689); -var utils = __webpack_require__(667); +var Cache = __webpack_require__(690); +var define = __webpack_require__(655); +var debug = __webpack_require__(545)('snapdragon:parser'); +var Position = __webpack_require__(691); +var utils = __webpack_require__(669); /** * Create a new `Parser` with the given `input` and `options`. @@ -79931,7 +80033,7 @@ module.exports = Parser; /***/ }), -/* 688 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80038,13 +80140,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 689 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(653); +var define = __webpack_require__(655); /** * Store position for a node @@ -80059,14 +80161,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 690 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(691); -var assignSymbols = __webpack_require__(598); +var isExtendable = __webpack_require__(693); +var assignSymbols = __webpack_require__(600); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -80126,7 +80228,7 @@ function isEnum(obj, key) { /***/ }), -/* 691 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80139,7 +80241,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(597); +var isPlainObject = __webpack_require__(599); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -80147,14 +80249,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 692 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(693); -var extglob = __webpack_require__(707); +var nanomatch = __webpack_require__(695); +var extglob = __webpack_require__(709); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -80231,7 +80333,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 693 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80242,17 +80344,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(112); -var toRegex = __webpack_require__(582); -var extend = __webpack_require__(694); +var toRegex = __webpack_require__(584); +var extend = __webpack_require__(696); /** * Local dependencies */ -var compilers = __webpack_require__(696); -var parsers = __webpack_require__(697); -var cache = __webpack_require__(700); -var utils = __webpack_require__(702); +var compilers = __webpack_require__(698); +var parsers = __webpack_require__(699); +var cache = __webpack_require__(702); +var utils = __webpack_require__(704); var MAX_LENGTH = 1024 * 64; /** @@ -81076,14 +81178,14 @@ module.exports = nanomatch; /***/ }), -/* 694 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(695); -var assignSymbols = __webpack_require__(598); +var isExtendable = __webpack_require__(697); +var assignSymbols = __webpack_require__(600); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -81143,7 +81245,7 @@ function isEnum(obj, key) { /***/ }), -/* 695 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81156,7 +81258,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(597); +var isPlainObject = __webpack_require__(599); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -81164,7 +81266,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 696 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81510,15 +81612,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 697 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(599); -var toRegex = __webpack_require__(582); -var isOdd = __webpack_require__(698); +var regexNot = __webpack_require__(601); +var toRegex = __webpack_require__(584); +var isOdd = __webpack_require__(700); /** * Characters to use in negation regex (we want to "not" match @@ -81904,7 +82006,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 698 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81917,7 +82019,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(699); +var isNumber = __webpack_require__(701); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -81931,7 +82033,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 699 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81959,14 +82061,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 700 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(701))(); +module.exports = new (__webpack_require__(703))(); /***/ }), -/* 701 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81979,7 +82081,7 @@ module.exports = new (__webpack_require__(701))(); -var MapCache = __webpack_require__(688); +var MapCache = __webpack_require__(690); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -82101,7 +82203,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 702 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82114,14 +82216,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(703)(); -var Snapdragon = __webpack_require__(624); -utils.define = __webpack_require__(704); -utils.diff = __webpack_require__(705); -utils.extend = __webpack_require__(694); -utils.pick = __webpack_require__(706); -utils.typeOf = __webpack_require__(592); -utils.unique = __webpack_require__(602); +var isWindows = __webpack_require__(705)(); +var Snapdragon = __webpack_require__(626); +utils.define = __webpack_require__(706); +utils.diff = __webpack_require__(707); +utils.extend = __webpack_require__(696); +utils.pick = __webpack_require__(708); +utils.typeOf = __webpack_require__(594); +utils.unique = __webpack_require__(604); /** * Returns true if the given value is effectively an empty string @@ -82487,7 +82589,7 @@ utils.unixify = function(options) { /***/ }), -/* 703 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -82515,7 +82617,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 704 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82528,8 +82630,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(590); -var isDescriptor = __webpack_require__(591); +var isobject = __webpack_require__(592); +var isDescriptor = __webpack_require__(593); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -82560,7 +82662,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 705 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82614,7 +82716,7 @@ function diffArray(one, two) { /***/ }), -/* 706 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82627,7 +82729,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(590); +var isObject = __webpack_require__(592); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -82656,7 +82758,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 707 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82666,18 +82768,18 @@ module.exports = function pick(obj, keys) { * Module dependencies */ -var extend = __webpack_require__(603); -var unique = __webpack_require__(602); -var toRegex = __webpack_require__(582); +var extend = __webpack_require__(605); +var unique = __webpack_require__(604); +var toRegex = __webpack_require__(584); /** * Local dependencies */ -var compilers = __webpack_require__(708); -var parsers = __webpack_require__(714); -var Extglob = __webpack_require__(717); -var utils = __webpack_require__(716); +var compilers = __webpack_require__(710); +var parsers = __webpack_require__(716); +var Extglob = __webpack_require__(719); +var utils = __webpack_require__(718); var MAX_LENGTH = 1024 * 64; /** @@ -82994,13 +83096,13 @@ module.exports = extglob; /***/ }), -/* 708 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(709); +var brackets = __webpack_require__(711); /** * Extglob compilers @@ -83170,7 +83272,7 @@ module.exports = function(extglob) { /***/ }), -/* 709 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83180,17 +83282,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(710); -var parsers = __webpack_require__(712); +var compilers = __webpack_require__(712); +var parsers = __webpack_require__(714); /** * Module dependencies */ -var debug = __webpack_require__(543)('expand-brackets'); -var extend = __webpack_require__(603); -var Snapdragon = __webpack_require__(624); -var toRegex = __webpack_require__(582); +var debug = __webpack_require__(545)('expand-brackets'); +var extend = __webpack_require__(605); +var Snapdragon = __webpack_require__(626); +var toRegex = __webpack_require__(584); /** * Parses the given POSIX character class `pattern` and returns a @@ -83388,13 +83490,13 @@ module.exports = brackets; /***/ }), -/* 710 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(711); +var posix = __webpack_require__(713); module.exports = function(brackets) { brackets.compiler @@ -83482,7 +83584,7 @@ module.exports = function(brackets) { /***/ }), -/* 711 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83511,14 +83613,14 @@ module.exports = { /***/ }), -/* 712 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(713); -var define = __webpack_require__(653); +var utils = __webpack_require__(715); +var define = __webpack_require__(655); /** * Text regex @@ -83737,14 +83839,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 713 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(582); -var regexNot = __webpack_require__(599); +var toRegex = __webpack_require__(584); +var regexNot = __webpack_require__(601); var cached; /** @@ -83778,15 +83880,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 714 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(709); -var define = __webpack_require__(715); -var utils = __webpack_require__(716); +var brackets = __webpack_require__(711); +var define = __webpack_require__(717); +var utils = __webpack_require__(718); /** * Characters to use in text regex (we want to "not" match @@ -83941,7 +84043,7 @@ module.exports = parsers; /***/ }), -/* 715 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83954,7 +84056,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(591); +var isDescriptor = __webpack_require__(593); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83979,14 +84081,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 716 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(599); -var Cache = __webpack_require__(701); +var regex = __webpack_require__(601); +var Cache = __webpack_require__(703); /** * Utils @@ -84055,7 +84157,7 @@ utils.createRegex = function(str) { /***/ }), -/* 717 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84065,16 +84167,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(624); -var define = __webpack_require__(715); -var extend = __webpack_require__(603); +var Snapdragon = __webpack_require__(626); +var define = __webpack_require__(717); +var extend = __webpack_require__(605); /** * Local dependencies */ -var compilers = __webpack_require__(708); -var parsers = __webpack_require__(714); +var compilers = __webpack_require__(710); +var parsers = __webpack_require__(716); /** * Customize Snapdragon parser and renderer @@ -84140,16 +84242,16 @@ module.exports = Extglob; /***/ }), -/* 718 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(707); -var nanomatch = __webpack_require__(693); -var regexNot = __webpack_require__(599); -var toRegex = __webpack_require__(582); +var extglob = __webpack_require__(709); +var nanomatch = __webpack_require__(695); +var regexNot = __webpack_require__(601); +var toRegex = __webpack_require__(584); var not; /** @@ -84230,14 +84332,14 @@ function textRegex(pattern) { /***/ }), -/* 719 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(701))(); +module.exports = new (__webpack_require__(703))(); /***/ }), -/* 720 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84250,13 +84352,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(624); -utils.define = __webpack_require__(721); -utils.diff = __webpack_require__(705); -utils.extend = __webpack_require__(690); -utils.pick = __webpack_require__(706); -utils.typeOf = __webpack_require__(592); -utils.unique = __webpack_require__(602); +var Snapdragon = __webpack_require__(626); +utils.define = __webpack_require__(723); +utils.diff = __webpack_require__(707); +utils.extend = __webpack_require__(692); +utils.pick = __webpack_require__(708); +utils.typeOf = __webpack_require__(594); +utils.unique = __webpack_require__(604); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -84553,7 +84655,7 @@ utils.unixify = function(options) { /***/ }), -/* 721 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84566,8 +84668,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(590); -var isDescriptor = __webpack_require__(591); +var isobject = __webpack_require__(592); +var isDescriptor = __webpack_require__(593); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -84598,7 +84700,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 722 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84617,9 +84719,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(723); -var reader_1 = __webpack_require__(736); -var fs_stream_1 = __webpack_require__(740); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -84680,15 +84782,15 @@ exports.default = ReaderAsync; /***/ }), -/* 723 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(724); -const readdirAsync = __webpack_require__(732); -const readdirStream = __webpack_require__(735); +const readdirSync = __webpack_require__(726); +const readdirAsync = __webpack_require__(734); +const readdirStream = __webpack_require__(737); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -84772,7 +84874,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 724 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84780,11 +84882,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(725); +const DirectoryReader = __webpack_require__(727); let syncFacade = { - fs: __webpack_require__(730), - forEach: __webpack_require__(731), + fs: __webpack_require__(732), + forEach: __webpack_require__(733), sync: true }; @@ -84813,7 +84915,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 725 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84822,9 +84924,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(726); -const stat = __webpack_require__(728); -const call = __webpack_require__(729); +const normalizeOptions = __webpack_require__(728); +const stat = __webpack_require__(730); +const call = __webpack_require__(731); /** * Asynchronously reads the contents of a directory and streams the results @@ -85200,14 +85302,14 @@ module.exports = DirectoryReader; /***/ }), -/* 726 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(727); +const globToRegExp = __webpack_require__(729); module.exports = normalizeOptions; @@ -85384,7 +85486,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 727 */ +/* 729 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -85521,13 +85623,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 728 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(729); +const call = __webpack_require__(731); module.exports = stat; @@ -85602,7 +85704,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 729 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85663,14 +85765,14 @@ function callOnce (fn) { /***/ }), -/* 730 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(729); +const call = __webpack_require__(731); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -85734,7 +85836,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 731 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85763,7 +85865,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 732 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85771,12 +85873,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(733); -const DirectoryReader = __webpack_require__(725); +const maybe = __webpack_require__(735); +const DirectoryReader = __webpack_require__(727); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(734), + forEach: __webpack_require__(736), async: true }; @@ -85818,7 +85920,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 733 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85845,7 +85947,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 734 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85881,7 +85983,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 735 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85889,11 +85991,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(725); +const DirectoryReader = __webpack_require__(727); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(734), + forEach: __webpack_require__(736), async: true }; @@ -85913,16 +86015,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 736 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(737); -var entry_1 = __webpack_require__(739); -var pathUtil = __webpack_require__(738); +var deep_1 = __webpack_require__(739); +var entry_1 = __webpack_require__(741); +var pathUtil = __webpack_require__(740); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -85988,14 +86090,14 @@ exports.default = Reader; /***/ }), -/* 737 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(738); -var patternUtils = __webpack_require__(576); +var pathUtils = __webpack_require__(740); +var patternUtils = __webpack_require__(578); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -86078,7 +86180,7 @@ exports.default = DeepFilter; /***/ }), -/* 738 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86109,14 +86211,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 739 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(738); -var patternUtils = __webpack_require__(576); +var pathUtils = __webpack_require__(740); +var patternUtils = __webpack_require__(578); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -86201,7 +86303,7 @@ exports.default = EntryFilter; /***/ }), -/* 740 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86221,8 +86323,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(741); -var fs_1 = __webpack_require__(745); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -86272,14 +86374,14 @@ exports.default = FileSystemStream; /***/ }), -/* 741 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(742); -const statProvider = __webpack_require__(744); +const optionsManager = __webpack_require__(744); +const statProvider = __webpack_require__(746); /** * Asynchronous API. */ @@ -86310,13 +86412,13 @@ exports.statSync = statSync; /***/ }), -/* 742 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(743); +const fsAdapter = __webpack_require__(745); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -86329,7 +86431,7 @@ exports.prepare = prepare; /***/ }), -/* 743 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86352,7 +86454,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 744 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86404,7 +86506,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 745 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86435,7 +86537,7 @@ exports.default = FileSystem; /***/ }), -/* 746 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86455,9 +86557,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(723); -var reader_1 = __webpack_require__(736); -var fs_stream_1 = __webpack_require__(740); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -86525,7 +86627,7 @@ exports.default = ReaderStream; /***/ }), -/* 747 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86544,9 +86646,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(723); -var reader_1 = __webpack_require__(736); -var fs_sync_1 = __webpack_require__(748); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_sync_1 = __webpack_require__(750); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -86606,7 +86708,7 @@ exports.default = ReaderSync; /***/ }), -/* 748 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86625,8 +86727,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(741); -var fs_1 = __webpack_require__(745); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -86672,7 +86774,7 @@ exports.default = FileSystemSync; /***/ }), -/* 749 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86688,7 +86790,7 @@ exports.flatten = flatten; /***/ }), -/* 750 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86709,13 +86811,13 @@ exports.merge = merge; /***/ }), -/* 751 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(752); +const pathType = __webpack_require__(754); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -86781,13 +86883,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 752 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(753); +const pify = __webpack_require__(755); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -86830,7 +86932,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 753 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86921,17 +87023,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 754 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(572); -const gitIgnore = __webpack_require__(755); -const pify = __webpack_require__(756); -const slash = __webpack_require__(757); +const fastGlob = __webpack_require__(574); +const gitIgnore = __webpack_require__(757); +const pify = __webpack_require__(758); +const slash = __webpack_require__(759); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -87029,7 +87131,7 @@ module.exports.sync = options => { /***/ }), -/* 755 */ +/* 757 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -87498,7 +87600,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 756 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87573,7 +87675,7 @@ module.exports = (input, options) => { /***/ }), -/* 757 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87591,7 +87693,7 @@ module.exports = input => { /***/ }), -/* 758 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87604,7 +87706,7 @@ module.exports = input => { -var isGlob = __webpack_require__(759); +var isGlob = __webpack_require__(761); module.exports = function hasGlob(val) { if (val == null) return false; @@ -87624,7 +87726,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 759 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -87655,17 +87757,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 760 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(761); -const CpFileError = __webpack_require__(764); -const fs = __webpack_require__(766); -const ProgressEmitter = __webpack_require__(769); +const pEvent = __webpack_require__(763); +const CpFileError = __webpack_require__(766); +const fs = __webpack_require__(768); +const ProgressEmitter = __webpack_require__(771); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -87779,12 +87881,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 761 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(762); +const pTimeout = __webpack_require__(764); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -88075,12 +88177,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 762 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(763); +const pFinally = __webpack_require__(765); class TimeoutError extends Error { constructor(message) { @@ -88126,7 +88228,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 763 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88148,12 +88250,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 764 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(765); +const NestedError = __webpack_require__(767); class CpFileError extends NestedError { constructor(message, nested) { @@ -88167,7 +88269,7 @@ module.exports = CpFileError; /***/ }), -/* 765 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -88223,16 +88325,16 @@ module.exports = NestedError; /***/ }), -/* 766 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(767); -const pEvent = __webpack_require__(761); -const CpFileError = __webpack_require__(764); +const makeDir = __webpack_require__(769); +const pEvent = __webpack_require__(763); +const CpFileError = __webpack_require__(766); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -88329,7 +88431,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 767 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88337,7 +88439,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(768); +const semver = __webpack_require__(770); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -88492,7 +88594,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 768 */ +/* 770 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -90094,7 +90196,7 @@ function coerce (version, options) { /***/ }), -/* 769 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90135,7 +90237,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 770 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90181,12 +90283,12 @@ exports.default = module.exports; /***/ }), -/* 771 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(772); +const pMap = __webpack_require__(774); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -90203,7 +90305,7 @@ module.exports.default = pFilter; /***/ }), -/* 772 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90282,12 +90384,12 @@ module.exports.default = pMap; /***/ }), -/* 773 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(765); +const NestedError = __webpack_require__(767); class CpyError extends NestedError { constructor(message, nested) { @@ -90301,7 +90403,7 @@ module.exports = CpyError; /***/ }), -/* 774 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90309,10 +90411,10 @@ module.exports = CpyError; const fs = __webpack_require__(134); const arrayUnion = __webpack_require__(145); const merge2 = __webpack_require__(146); -const fastGlob = __webpack_require__(775); +const fastGlob = __webpack_require__(777); const dirGlob = __webpack_require__(232); -const gitignore = __webpack_require__(810); -const {FilterStream, UniqueStream} = __webpack_require__(811); +const gitignore = __webpack_require__(812); +const {FilterStream, UniqueStream} = __webpack_require__(813); const DEFAULT_FILTER = () => false; @@ -90489,17 +90591,17 @@ module.exports.gitignore = gitignore; /***/ }), -/* 775 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(776); -const async_1 = __webpack_require__(796); -const stream_1 = __webpack_require__(806); -const sync_1 = __webpack_require__(807); -const settings_1 = __webpack_require__(809); -const utils = __webpack_require__(777); +const taskManager = __webpack_require__(778); +const async_1 = __webpack_require__(798); +const stream_1 = __webpack_require__(808); +const sync_1 = __webpack_require__(809); +const settings_1 = __webpack_require__(811); +const utils = __webpack_require__(779); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -90563,14 +90665,14 @@ module.exports = FastGlob; /***/ }), -/* 776 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; -const utils = __webpack_require__(777); +const utils = __webpack_require__(779); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -90635,31 +90737,31 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 777 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; -const array = __webpack_require__(778); +const array = __webpack_require__(780); exports.array = array; -const errno = __webpack_require__(779); +const errno = __webpack_require__(781); exports.errno = errno; -const fs = __webpack_require__(780); +const fs = __webpack_require__(782); exports.fs = fs; -const path = __webpack_require__(781); +const path = __webpack_require__(783); exports.path = path; -const pattern = __webpack_require__(782); +const pattern = __webpack_require__(784); exports.pattern = pattern; -const stream = __webpack_require__(794); +const stream = __webpack_require__(796); exports.stream = stream; -const string = __webpack_require__(795); +const string = __webpack_require__(797); exports.string = string; /***/ }), -/* 778 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90688,7 +90790,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 779 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90702,7 +90804,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 780 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90728,7 +90830,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 781 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90768,7 +90870,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 782 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90777,7 +90879,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; const path = __webpack_require__(4); const globParent = __webpack_require__(171); -const micromatch = __webpack_require__(783); +const micromatch = __webpack_require__(785); const picomatch = __webpack_require__(185); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; @@ -90907,14 +91009,14 @@ exports.matchAny = matchAny; /***/ }), -/* 783 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(112); -const braces = __webpack_require__(784); +const braces = __webpack_require__(786); const picomatch = __webpack_require__(185); const utils = __webpack_require__(188); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); @@ -91381,16 +91483,16 @@ module.exports = micromatch; /***/ }), -/* 784 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(785); -const compile = __webpack_require__(787); -const expand = __webpack_require__(791); -const parse = __webpack_require__(792); +const stringify = __webpack_require__(787); +const compile = __webpack_require__(789); +const expand = __webpack_require__(793); +const parse = __webpack_require__(794); /** * Expand the given pattern or create a regex-compatible string. @@ -91558,13 +91660,13 @@ module.exports = braces; /***/ }), -/* 785 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(786); +const utils = __webpack_require__(788); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -91597,7 +91699,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 786 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91716,14 +91818,14 @@ exports.flatten = (...args) => { /***/ }), -/* 787 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(788); -const utils = __webpack_require__(786); +const fill = __webpack_require__(790); +const utils = __webpack_require__(788); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -91780,7 +91882,7 @@ module.exports = compile; /***/ }), -/* 788 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91794,7 +91896,7 @@ module.exports = compile; const util = __webpack_require__(112); -const toRegexRange = __webpack_require__(789); +const toRegexRange = __webpack_require__(791); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -92036,7 +92138,7 @@ module.exports = fill; /***/ }), -/* 789 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92049,7 +92151,7 @@ module.exports = fill; -const isNumber = __webpack_require__(790); +const isNumber = __webpack_require__(792); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -92331,7 +92433,7 @@ module.exports = toRegexRange; /***/ }), -/* 790 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92356,15 +92458,15 @@ module.exports = function(num) { /***/ }), -/* 791 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(788); -const stringify = __webpack_require__(785); -const utils = __webpack_require__(786); +const fill = __webpack_require__(790); +const stringify = __webpack_require__(787); +const utils = __webpack_require__(788); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -92476,13 +92578,13 @@ module.exports = expand; /***/ }), -/* 792 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(785); +const stringify = __webpack_require__(787); /** * Constants @@ -92504,7 +92606,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(793); +} = __webpack_require__(795); /** * parse @@ -92816,7 +92918,7 @@ module.exports = parse; /***/ }), -/* 793 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92880,7 +92982,7 @@ module.exports = { /***/ }), -/* 794 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92904,7 +93006,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 795 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92922,14 +93024,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 796 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(797); -const provider_1 = __webpack_require__(799); +const stream_1 = __webpack_require__(799); +const provider_1 = __webpack_require__(801); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -92957,7 +93059,7 @@ exports.default = ProviderAsync; /***/ }), -/* 797 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92966,7 +93068,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); const fsStat = __webpack_require__(195); const fsWalk = __webpack_require__(200); -const reader_1 = __webpack_require__(798); +const reader_1 = __webpack_require__(800); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -93019,7 +93121,7 @@ exports.default = ReaderStream; /***/ }), -/* 798 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93027,7 +93129,7 @@ exports.default = ReaderStream; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); const fsStat = __webpack_require__(195); -const utils = __webpack_require__(777); +const utils = __webpack_require__(779); class Reader { constructor(_settings) { this._settings = _settings; @@ -93059,17 +93161,17 @@ exports.default = Reader; /***/ }), -/* 799 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(800); -const entry_1 = __webpack_require__(803); -const error_1 = __webpack_require__(804); -const entry_2 = __webpack_require__(805); +const deep_1 = __webpack_require__(802); +const entry_1 = __webpack_require__(805); +const error_1 = __webpack_require__(806); +const entry_2 = __webpack_require__(807); class Provider { constructor(_settings) { this._settings = _settings; @@ -93114,14 +93216,14 @@ exports.default = Provider; /***/ }), -/* 800 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); -const partial_1 = __webpack_require__(801); +const utils = __webpack_require__(779); +const partial_1 = __webpack_require__(803); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -93183,13 +93285,13 @@ exports.default = DeepFilter; /***/ }), -/* 801 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(802); +const matcher_1 = __webpack_require__(804); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -93228,13 +93330,13 @@ exports.default = PartialMatcher; /***/ }), -/* 802 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(779); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -93285,13 +93387,13 @@ exports.default = Matcher; /***/ }), -/* 803 */ +/* 805 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(779); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -93348,13 +93450,13 @@ exports.default = EntryFilter; /***/ }), -/* 804 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(779); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -93370,13 +93472,13 @@ exports.default = ErrorFilter; /***/ }), -/* 805 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(779); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -93403,15 +93505,15 @@ exports.default = EntryTransformer; /***/ }), -/* 806 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const stream_2 = __webpack_require__(797); -const provider_1 = __webpack_require__(799); +const stream_2 = __webpack_require__(799); +const provider_1 = __webpack_require__(801); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -93441,14 +93543,14 @@ exports.default = ProviderStream; /***/ }), -/* 807 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(808); -const provider_1 = __webpack_require__(799); +const sync_1 = __webpack_require__(810); +const provider_1 = __webpack_require__(801); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -93471,7 +93573,7 @@ exports.default = ProviderSync; /***/ }), -/* 808 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93479,7 +93581,7 @@ exports.default = ProviderSync; Object.defineProperty(exports, "__esModule", { value: true }); const fsStat = __webpack_require__(195); const fsWalk = __webpack_require__(200); -const reader_1 = __webpack_require__(798); +const reader_1 = __webpack_require__(800); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -93521,7 +93623,7 @@ exports.default = ReaderSync; /***/ }), -/* 809 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93585,7 +93687,7 @@ exports.default = Settings; /***/ }), -/* 810 */ +/* 812 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93593,7 +93695,7 @@ exports.default = Settings; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(775); +const fastGlob = __webpack_require__(777); const gitIgnore = __webpack_require__(235); const slash = __webpack_require__(236); @@ -93712,7 +93814,7 @@ module.exports.sync = options => { /***/ }), -/* 811 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93765,7 +93867,7 @@ module.exports = { /***/ }), -/* 812 */ +/* 814 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -93773,13 +93875,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return buildNonBazelProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProductionProjects", function() { return getProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProject", function() { return buildProject; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(567); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(562); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(564); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index b383a52be63f5..bad6eef3266f8 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -66,7 +66,7 @@ export const BootstrapCommand: ICommand = { await runBazel(['run', '@nodejs//:yarn'], runOffline); } - await runBazel(['build', '//packages:build'], runOffline); + await runBazel(['build', '//packages:build', '--show_result=1'], runOffline); // Install monorepo npm dependencies outside of the Bazel managed ones for (const batch of batchedNonBazelProjects) { diff --git a/packages/kbn-pm/src/commands/build_bazel.ts b/packages/kbn-pm/src/commands/build_bazel.ts new file mode 100644 index 0000000000000..f71e2e96e31b0 --- /dev/null +++ b/packages/kbn-pm/src/commands/build_bazel.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { runBazel } from '../utils/bazel'; +import { ICommand } from './'; + +export const BuildBazelCommand: ICommand = { + description: 'Runs a build in the Bazel built packages', + name: 'build-bazel', + + async run(projects, projectGraph, { options }) { + const runOffline = options?.offline === true; + + // Call bazel with the target to build all available packages + await runBazel(['build', '//packages:build', '--show_result=1'], runOffline); + }, +}; diff --git a/packages/kbn-pm/src/commands/index.ts b/packages/kbn-pm/src/commands/index.ts index 0ab6bc9c7808a..2f5c04c2f434f 100644 --- a/packages/kbn-pm/src/commands/index.ts +++ b/packages/kbn-pm/src/commands/index.ts @@ -27,16 +27,20 @@ export interface ICommand { } import { BootstrapCommand } from './bootstrap'; +import { BuildBazelCommand } from './build_bazel'; import { CleanCommand } from './clean'; import { ResetCommand } from './reset'; import { RunCommand } from './run'; import { WatchCommand } from './watch'; +import { WatchBazelCommand } from './watch_bazel'; import { Kibana } from '../utils/kibana'; export const commands: { [key: string]: ICommand } = { bootstrap: BootstrapCommand, + 'build-bazel': BuildBazelCommand, clean: CleanCommand, reset: ResetCommand, run: RunCommand, watch: WatchCommand, + 'watch-bazel': WatchBazelCommand, }; diff --git a/packages/kbn-pm/src/commands/run.ts b/packages/kbn-pm/src/commands/run.ts index 5535fe0d8358f..9a3a19d9e625e 100644 --- a/packages/kbn-pm/src/commands/run.ts +++ b/packages/kbn-pm/src/commands/run.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import dedent from 'dedent'; import { CliError } from '../utils/errors'; import { log } from '../utils/log'; import { parallelizeBatches } from '../utils/parallelize'; @@ -13,10 +14,17 @@ import { topologicallyBatchProjects } from '../utils/projects'; import { ICommand } from './'; export const RunCommand: ICommand = { - description: 'Run script defined in package.json in each package that contains that script.', + description: + 'Run script defined in package.json in each package that contains that script (only works on packages not using Bazel yet)', name: 'run', async run(projects, projectGraph, { extraArgs, options }) { + log.warning(dedent` + We are migrating packages into the Bazel build system and we will no longer support running npm scripts on + packages using 'yarn kbn run' on Bazel built packages. If the package you are trying to act on contains a + BUILD.bazel file please just use 'yarn kbn build-bazel' to build it or 'yarn kbn watch-bazel' to watch it + `); + const batchedProjects = topologicallyBatchProjects(projects, projectGraph); if (extraArgs.length === 0) { diff --git a/packages/kbn-pm/src/commands/watch.ts b/packages/kbn-pm/src/commands/watch.ts index fb398d6852136..5d0f6d086d3e8 100644 --- a/packages/kbn-pm/src/commands/watch.ts +++ b/packages/kbn-pm/src/commands/watch.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import dedent from 'dedent'; import { CliError } from '../utils/errors'; import { log } from '../utils/log'; import { parallelizeBatches } from '../utils/parallelize'; @@ -34,10 +35,16 @@ const kibanaProjectName = 'kibana'; * `webpack` and `tsc` only, for the rest we rely on predefined timeouts. */ export const WatchCommand: ICommand = { - description: 'Runs `kbn:watch` script for every project.', + description: + 'Runs `kbn:watch` script for every project (only works on packages not using Bazel yet)', name: 'watch', async run(projects, projectGraph) { + log.warning(dedent` + We are migrating packages into the Bazel build system. If the package you are trying to watch + contains a BUILD.bazel file please just use 'yarn kbn watch-bazel' + `); + const projectsToWatch: ProjectMap = new Map(); for (const project of projects.values()) { // We can't watch project that doesn't have `kbn:watch` script. diff --git a/packages/kbn-pm/src/commands/watch_bazel.ts b/packages/kbn-pm/src/commands/watch_bazel.ts new file mode 100644 index 0000000000000..1273562dd2511 --- /dev/null +++ b/packages/kbn-pm/src/commands/watch_bazel.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { runIBazel } from '../utils/bazel'; +import { ICommand } from './'; + +export const WatchBazelCommand: ICommand = { + description: 'Runs a build in the Bazel built packages and keeps watching them for changes', + name: 'watch-bazel', + + async run(projects, projectGraph, { options }) { + const runOffline = options?.offline === true; + + // Call bazel with the target to build all available packages and run it through iBazel to watch it for changes + // + // Note: --run_output=false arg will disable the iBazel notifications about gazelle and buildozer when running it + // Can also be solved by adding a root `.bazel_fix_commands.json` but its not needed at the moment + await runIBazel(['--run_output=false', 'build', '//packages:build'], runOffline); + }, +}; diff --git a/packages/kbn-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts index ab20150768b78..34718606db98e 100644 --- a/packages/kbn-pm/src/utils/bazel/run.ts +++ b/packages/kbn-pm/src/utils/bazel/run.ts @@ -13,8 +13,12 @@ import { tap } from 'rxjs/operators'; import { observeLines } from '@kbn/dev-utils/stdio'; import { spawn } from '../child_process'; import { log } from '../log'; +import { CliError } from '../errors'; -export async function runBazel( +type BazelCommandRunner = 'bazel' | 'ibazel'; + +async function runBazelCommandWithRunner( + bazelCommandRunner: BazelCommandRunner, bazelArgs: string[], offline: boolean = false, runOpts: execa.Options = {} @@ -29,7 +33,7 @@ export async function runBazel( bazelArgs.push('--config=offline'); } - const bazelProc = spawn('bazel', bazelArgs, bazelOpts); + const bazelProc = spawn(bazelCommandRunner, bazelArgs, bazelOpts); const bazelLogs$ = new Rx.Subject(); @@ -37,15 +41,35 @@ export async function runBazel( // Therefore we need to get both. In order to get errors we need to parse the actual text line const bazelLogSubscription = Rx.merge( observeLines(bazelProc.stdout!).pipe( - tap((line) => log.info(`${chalk.cyan('[bazel]')} ${line}`)) + tap((line) => log.info(`${chalk.cyan(`[${bazelCommandRunner}]`)} ${line}`)) ), observeLines(bazelProc.stderr!).pipe( - tap((line) => log.info(`${chalk.cyan('[bazel]')} ${line}`)) + tap((line) => log.info(`${chalk.cyan(`[${bazelCommandRunner}]`)} ${line}`)) ) ).subscribe(bazelLogs$); // Wait for process and logs to finish, unsubscribing in the end - await bazelProc; + try { + await bazelProc; + } catch { + throw new CliError(`The bazel command that was running failed to complete.`); + } await bazelLogs$.toPromise(); await bazelLogSubscription.unsubscribe(); } + +export async function runBazel( + bazelArgs: string[], + offline: boolean = false, + runOpts: execa.Options = {} +) { + await runBazelCommandWithRunner('bazel', bazelArgs, offline, runOpts); +} + +export async function runIBazel( + bazelArgs: string[], + offline: boolean = false, + runOpts: execa.Options = {} +) { + await runBazelCommandWithRunner('ibazel', bazelArgs, offline, runOpts); +} diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js index abbf911cfc8fc..a9ec32023f2bf 100644 --- a/packages/kbn-ui-shared-deps/polyfills.js +++ b/packages/kbn-ui-shared-deps/polyfills.js @@ -8,7 +8,6 @@ require('core-js/stable'); require('regenerator-runtime/runtime'); -require('custom-event-polyfill'); if (typeof window.Event === 'object') { // IE11 doesn't support unknown event types, required by react-use @@ -17,6 +16,4 @@ if (typeof window.Event === 'object') { } require('whatwg-fetch'); -require('abortcontroller-polyfill/dist/polyfill-patch-fetch'); -require('./vendor/childnode_remove_polyfill'); require('symbol-observable'); diff --git a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js b/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js deleted file mode 100644 index d8818fe809ccb..0000000000000 --- a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -/* @notice - * This product bundles childnode-remove which is available under a - * "MIT" license. - * - * The MIT License (MIT) - * - * Copyright (c) 2016-present, jszhou - * https://github.com/jserz/js_piece - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* eslint-disable */ - -(function (arr) { - arr.forEach(function (item) { - if (item.hasOwnProperty('remove')) { - return; - } - Object.defineProperty(item, 'remove', { - configurable: true, - enumerable: true, - writable: true, - value: function remove() { - if (this.parentNode !== null) - this.parentNode.removeChild(this); - } - }); - }); -})([Element.prototype, CharacterData.prototype, DocumentType.prototype]); diff --git a/rfcs/images/ols_phase_1_auth.png b/rfcs/images/ols_phase_1_auth.png new file mode 100644 index 0000000000000..5bf4b210bee9e Binary files /dev/null and b/rfcs/images/ols_phase_1_auth.png differ diff --git a/rfcs/text/0016_ols_phase_1.md b/rfcs/text/0016_ols_phase_1.md new file mode 100644 index 0000000000000..c1f65111df328 --- /dev/null +++ b/rfcs/text/0016_ols_phase_1.md @@ -0,0 +1,323 @@ +- Start Date: 2020-03-01 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +--- +- [1. Summary](#1-summary) +- [2. Motivation](#2-motivation) +- [3. Detailed design](#3-detailed-design) +- [4. Drawbacks](#4-drawbacks) +- [5. Alternatives](#5-alternatives) +- [6. Adoption strategy](#6-adoption-strategy) +- [7. How we teach this](#7-how-we-teach-this) +- [8. Unresolved questions](#8-unresolved-questions) + +# 1. Summary + +Object-level security ("OLS") authorizes Saved Object CRUD operations on a per-object basis. +This RFC focuses on the [phase 1](https://github.com/elastic/kibana/issues/82725), which introduces "private" saved object types. These private types +are owned by individual users, and are _generally_ only accessible by their owners. + +This RFC does not address any [followup phases](https://github.com/elastic/kibana/issues/39259), which may support sharing, and ownership of "public" objects. + +# 2. Motivation + +OLS allows saved objects to be owned by individual users. This allows Kibana to store information that is specific +to each user, which enables further customization and collaboration throughout our solutions. + +The most immediate feature this unlocks is [User settings and preferences (#17888)](https://github.com/elastic/kibana/issues/17888), +which is a very popular and long-standing request. + +# 3. Detailed design + +Phase 1 of OLS allows consumers to register "private" saved object types. +These saved objects are owned by individual end users, and are subject to additional security controls. + +Public (non-private) saved object types are not impacted by this RFC. This proposal does not allow types to transition to/from `public`/`private`, and is considered out of scope for phase 1. + +## 3.1 Saved Objects Service + +### 3.1.1 Type registry +The [saved objects type registry](https://github.com/elastic/kibana/blob/701697cc4a34d07c0508c3bdf01dca6f9d40a636/src/core/server/saved_objects/saved_objects_type_registry.ts) will allow consumers to register "private" saved object types via a new `accessClassification` property: + +```ts +/** + * The accessClassification dictates the protection level of the saved object: + * * public (default): instances of this saved object type will be accessible to all users within the given namespace, who are authorized to act on objects of this type. + * * private: instances of this saved object type will belong to the user who created them, and will not be accessible by other users, except for administrators. + */ +export type SavedObjectsAccessClassification = 'public' | 'private'; + +// Note: some existing properties have been omitted for brevity. +export interface SavedObjectsType { + name: string; + hidden: boolean; + namespaceType: SavedObjectsNamespaceType; + mappings: SavedObjectsTypeMappingDefinition; + + /** + * The {@link SavedObjectsAccessClassification | accessClassification} for the type. + */ + accessClassification?: SavedObjectsAccessClassification; +} + +// Example consumer +class MyPlugin { + setup(core: CoreSetup) { + core.savedObjects.registerType({ + name: 'user-settings', + accessClassification: 'private', + namespaceType: 'single', + hidden: false, + mappings, + }) + } +} +``` + +### 3.1.2 Schema +Saved object ownership will be recorded as metadata within each `private` saved object. We do so by adding a top-level `accessControl` object with a singular `owner` property. See [unresolved question 1](#81-accessControl.owner) for details on the `owner` property. + +```ts +/** + * Describes which users should be authorized to access this SavedObject. + * + * @public + */ +export interface SavedObjectAccessControl { + /** The owner of this SavedObject. */ + owner: string; +} + +// Note: some existing fields have been omitted for brevity +export interface SavedObject { + id: string; + type: string; + attributes: T; + references: SavedObjectReference[]; + namespaces?: string[]; + /** Describes which users should be authorized to access this SavedObject. */ + accessControl?: SavedObjectAccessControl; +} +``` + +### 3.1.3 Saved Objects Client: Security wrapper + +The [security wrapper](https://github.com/elastic/kibana/blob/701697cc4a34d07c0508c3bdf01dca6f9d40a636/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts) authorizes and audits operations against saved objects. + +There are two primary changes to this wrapper: + +#### Attaching Access Controls + +This wrapper will be responsible for attaching an access control specification to all private objects before they are created in Elasticsearch. +It will also allow users to provide their own access control specification in order to support the import/create use cases. + +Similar to the way we treat `namespaces`, it will not be possible to change an access control specification via the `update`/`bulk_update` functions in this first phase. We may consider adding a dedicated function to update the access control specification, similar to what we've done for sharing to spaces. + +#### Authorization changes + +This wrapper will be updated to ensure that access to private objects is only granted to authorized users. A user is authorized to operate on a private saved object if **all of the following** are true: +Step 1) The user is authorized to perform the operation on saved objects of the requested type, within the requested space. (Example: `update` a `user-settings` saved object in the `marketing` space) +Step 2) The user is authorized to access this specific instance of the saved object, as described by that object's access control specification. For this first phase, the `accessControl.owner` is allowed to perform all operations. The only other users who are allowed to access this object are administrators (see [resolved question 2](#92-authorization-for-private-objects)) + +Step 1 of this authorization check is the same check we perform today for all existing saved object types. Step 2 is a new authorization check, and **introduces additional overhead and complexity**. We explore the logic for this step in more detail later in this RFC. Alternatives to this approach are discussed in [alternatives, section 5.2](#52-re-using-the-repositorys-pre-flight-checks). + +![High-level authorization model for private objects](../images/ols_phase_1_auth.png) + +## 3.2 Saved Objects API + +OLS Phase 1 does not introduce any new APIs, but rather augments the existing Saved Object APIs. + +APIs which return saved objects are augmented to include the top-level `accessControl` property when it exists. This includes the `export` API. + +APIs that create saved objects are augmented to accept an `accessControl` property. This includes the `import` API. + +### `get` / `bulk_get` + +The security wrapper will ensure the user is authorized to access private objects before returning them to the consumer. + +#### Performance considerations +None. The retrieved object contains all of the necessary information to authorize the current user, with no additional round trips to Elasticsearch. + +### `create` / `bulk_create` + +The security wrapper will ensure that an access control specification is attached to all private objects. + +If the caller has requested to overwrite existing `private` objects, then the security wrapper must ensure that the user is authorized to do so. + +#### Performance considerations +When overwriting existing objects, the security wrapper must first retrieve all of the existing `private` objects to ensure that the user is authorized. This requires another round-trip to `get`/`bulk-get` all `private` objects so we can authorize the operation. + +This overhead does not impact overwriting "public" objects. We only need to retrieve objects that are registered as `private`. As such, we do not expect any meaningful performance hit initially, but this will grow over time as the feature is used. + +### `update` / `bulk_update` + +The security wrapper will ensure that the user is authorized to update all existing `private` objects. It will also ensure that an access control specification is not provided, as updates to the access control specification are not permitted via `update`/`bulk_update`. + +#### Performance considerations +Similar to the "create / override" scenario above, the security wrapper must first retrieve all of the existing `private` objects to ensure that the user is authorized. This requires another round-trip to `get`/`bulk-get` all `private` objects so we can authorize the operation. + +This overhead does not impact updating "public" objects. We only need to retrieve objects that are registered as `private`. As such, we do not expect any meaningful performance hit initially, but this will grow over time as the feature is used. + +### `delete` + +The security wrapper will first retrieve the requested `private` object to ensure the user is authorized. + +#### Performance considerations +The security wrapper must first retrieve the existing `private` object to ensure that the user is authorized. This requires another round-trip to `get` the `private` object so we can authorize the operation. + +This overhead does not impact deleting "public" objects. We only need to retrieve objects that are registered as `private`. As such, we do not expect any meaningful performance hit initially, but this will grow over time as the feature is used. + + +### `find` +The security wrapper will supply or augment a [KQL `filter`](https://github.com/elastic/kibana/blob/701697cc4a34d07c0508c3bdf01dca6f9d40a636/src/core/server/saved_objects/types.ts#L118) which describes the objects the current user is authorized to see. + +```ts +// Sample KQL filter +const filterClauses = typesToFind.reduce((acc, type) => { + if (this.typeRegistry.isPrivate(type)) { + return [ + ...acc, + // note: this relies on specific behavior of the SO service's `filter_utils`, + // which automatically wraps this in an `and` node to ensure the type is accounted for. + // we have added additional safeguards there, and functional tests will ensure that changes + // to this logic will not accidentally alter our authorization model. + + // This is equivalent to writing the following, if this syntax was allowed by the SO `filter` option: + // esKuery.nodeTypes.function.buildNode('and', [ + // esKuery.nodeTypes.function.buildNode('is', `accessControl.owner`, this.getOwner()), + // esKuery.nodeTypes.function.buildNode('is', `type`, type), + // ]) + esKuery.nodeTypes.function.buildNode('is', `${type}.accessControl.owner`, this.getOwner()), + ]; + } + return acc; +}, []); + +const privateObjectsFilter = + filterClauses.length > 0 ? esKuery.nodeTypes.function.buildNode('or', filterClauses) : null; +``` + +#### Performance considerations +We are sending a more complex query to Elasticsearch for any find request which requests a `private` saved object. This has the potential to hurt query performance, but at this point it hasn't been quantified. + +Since we are only requesting saved objects that the user is authorized to see, there is no additional overhead for Kibana once Elasticsearch has returned the results of the query. + + +### `addToNamespaces` / `deleteFromNamespaces` + +The security wrapper will ensure that the user is authorized to share/unshare all existing `private` objects. +#### Performance considerations +Similar to the "create / override" scenario above, the security wrapper must first retrieve all of the existing `private` objects to ensure that the user is authorized. This requires another round-trip to `get`/`bulk-get` all `private` objects so we can authorize the operation. + +This overhead does not impact sharing/unsharing "public" objects. We only need to retrieve objects that are registered as `private`. As such, we do not expect any meaningful performance hit initially, but this will grow over time as the feature is used. + + +## 3.3 Behavior with various plugin configurations +Kibana can run with and without security enabled. When security is disabled, +`private` saved objects will be accessible to all users. + +| **Plugin Configuration** | Security | Security & Spaces | Spaces | +| ---- | ------ | ------ | --- | +|| ✅ Enforced | ✅ Enforced | 🚫 Not enforced: objects will be accessible to all + +### Alternative +If this behavior is not desired, we can prevent `private` saved objects from being accessed whenever security is disabled. + +See [unresolved question 3](#83-behavior-when-security-is-disabled) + +## 3.4 Impacts on telemetry + +The proposed design does not have any impacts on telemetry collection or reporting. Telemetry collectors run in the background against an "unwrapped" saved objects client. That is to say, they run without space-awareness, and without security. Since the security enforcement for private objects exists within the security wrapper, telemetry collection can continue as it currently exists. + +# 4. Drawbacks + +As outlined above, this approach introduces additional overhead to many of the saved object APIs. We minimize this by denoting which saved object types require this additional authorization. + +This first phase also does not allow a public object to become private. Search sessions may migrate to OLS in the future, but this will likely be a coordinated effort with Elasticsearch, due to the differing ownership models between OLS and async searches. + +# 5. Alternatives + +## 5.1 Document level security +OLS can be thought of as a Kibana-specific implementation of [Document level security](https://www.elastic.co/guide/en/elasticsearch/reference/current/document-level-security.html) ("DLS"). As such, we could consider enhancing the existing DLS feature to fit our needs (DLS doesn't prevent writes at the moment, only reads). This would involve considerable work from the Elasticsearch security team before we could consider this, and may not scale to subsequent phases of OLS. + +## 5.2 Re-using the repository's pre-flight checks +The Saved Objects Repository uses pre-flight checks to ensure that operations against multi-namespace saved objects are adhering the user's current space. The currently proposed implementation has the security wrapper performing pre-flight checks for `private` objects. + +If we have `private` multi-namespace saved objects, then we will end up performing two pre-flight requests, which is excessive. We could explore re-using the repository's pre-flight checks instead of introducing new checks. + +The primary concern with this approach is audit logging. Currently, we audit create/update/delete events before they happen, so that we can record that the operation was attempted, even in the event of a network outage or other transient event. + +If we re-use the repository's pre-flight checks, then the repository will need a way to signal that audit logging should occur. We have a couple of options to explore in this regard: + +### 5.2.1 Move audit logging code into the repository +Now that we no longer ship an OSS distribution, we could move the audit logging code directly into the repository. The implementation could still be provided by the security plugin, so we could still record information about the current user, and respect the current license. + +If we take this approach, then we will need a way to create a repository without audit logging. Certain features rely on the fact that the repository does not perform its own audit logging (such as Alerting, and the background repair jobs for ML). + +Core originally provided an [`audit_trail_service`](https://github.com/elastic/kibana/blob/v7.9.3/src/core/server/audit_trail/audit_trail_service.ts) for this type of functionality, with the thinking that OSS features could take advantage of this if needed. This was abandoned when we discovered that we had no such usages at the time, so we simplified the architecture. We could re-introduce this if desired, in order to support this initiative. + +Not all saved object audit events can be recorded by the repository. When users are not authorized at the type level (e.g., user can't `create` `dashboards`), then the wrapper will record this and not allow the operation to proceed. This shared-responsibility model will likely be even more confusing to reason about, so I'm not sure it's worth the small performance optimization we would get in return. + +### 5.2.2 Pluggable authorization +This inverts the current model. Instead of security wrapping the saved objects client, security could instead provide an authorization module to the repository. The repository could decide when to perform authorization (including audit logging), passing along the results of any pre-flight operations as necessary. + +This arguably a lot of work, but worth consideration as we evolve both our persistence and authorization mechanisms to support our maturing solutions. + +Similar to alternative `5.2.1`, we would need a way to create a repository without authorization/auditing to support specific use cases. + +### 5.2.3 Repository callbacks + +A more rudimentary approach would be to provide callbacks via each saved object operation's `options` property. This callback would be provided by the security wrapper, and called by the repository when it was "safe" to perform the audit operation. + +This is a very simplistic approach, and probably not an architecture that we want to encourage or support long-term. + +### 5.2.4 Pass down preflight objects + +Any client wrapper could fetch the object/s on its own and pass that down to the repository in an `options` field (preflightObject/s?) so the repository can reuse that result if it's defined, instead of initiating an entire additional preflight check. That resolves our problem without much additional complexity. +Of course we don't want consumers (mis)using this field, we can either mark it as `@internal` or we could explore creating a separate "internal SOC" interface that is only meant to be used by the SOC wrappers. + + +# 6. Adoption strategy + +Adoption for net-new features is hopefully straightforward. Like most saved object features, the saved objects service will transparently handle all authorization and auditing of these objects, so long as they are properly registered. + +Adoption for existing features (public saved object types) is not addressed in this first phase. + +# 7. How we teach this + +Updates to the saved object service's documentation to describe the different `accessClassification`s would be required. Like other saved object security controls, we want to ensure that engineers understand that this only "works" when the security wrapper is applied. Creating a bespoke instance of the saved objects client, or using the raw repository will intentionally bypass these authorization checks. + +# 8. Unresolved questions + +## 8.1 `accessControl.owner` + +The `accessControl.owner` property will uniquely identify the owner of each `private` saved object. We are still iterating with the Elasticsearch security team on what this value will ultimately look like. It is highly likely that this will not be a human-readable piece of text, but rather a GUID-style identifier. + +## 8.2 Authorization for private objects + +This has been [resolved](#92-authorization-for-private-objects). + +The user identified by `accessControl.owner` will be authorized for all operations against that instance, provided they pass the existing type/space/action authorization checks. + +In addition to the object owner, we also need to allow administrators to manage these saved objects. This is beneficial if they need to perform a bulk import/export of private objects, or if they wish to remove private objects from users that no longer exist. The open question is: **who counts as an administrator?** + +We have historically used the `Saved Objects Management` feature for these administrative tasks. This feature grants access to all saved objects, even if you're not authorized to access the "owning" application. Do we consider this privilege sufficient to see and potentially manipulate private saved objects? + +## 8.3 Behavior when security is disabled + +This has been [resolved](#93-behavior-when-security-is-disabled). + +When security is disabled, should `private` saved objects still be accessible via the Saved Objects Client? + + +# 9. Resolved Questions + +## 9.2 Authorization for private objects + +Users with the `Saved Objects Management` privilege will be authorized to access private saved objects belonging to other users. +Additionally, we will introduce a sub-feature privilege which will allow administrators to control which of their users with `Saved Objects Management` access are authorized to access these private objects. + +## 9.3 Behavior when security is disabled + +When security is disabled, `private` objects will still be accessible via the Saved Objects Client. \ No newline at end of file diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 5e2c54853d648..4629b9ffa07d9 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -274,8 +274,10 @@ export class DocLinksService { painlessExecute: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html`, painlessExecuteAPIContexts: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html#_contexts`, putComponentTemplateMetadata: `${ELASTICSEARCH_DOCS}indices-component-template.html#component-templates-metadata`, + putEnrichPolicy: `${ELASTICSEARCH_DOCS}put-enrich-policy-api.html`, putSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, - putWatch: `${ELASTICSEARCH_DOCS}/watcher-api-put-watch.html`, + putWatch: `${ELASTICSEARCH_DOCS}watcher-api-put-watch.html`, + simulatePipeline: `${ELASTICSEARCH_DOCS}simulate-pipeline-api.html`, updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`, }, plugins: { @@ -296,9 +298,47 @@ export class DocLinksService { restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, }, ingest: { + append: `${ELASTICSEARCH_DOCS}append-processor.html`, + bytes: `${ELASTICSEARCH_DOCS}bytes-processor.html`, + circle: `${ELASTICSEARCH_DOCS}ingest-circle-processor.html`, + convert: `${ELASTICSEARCH_DOCS}convert-processor.html`, + csv: `${ELASTICSEARCH_DOCS}csv-processor.html`, + date: `${ELASTICSEARCH_DOCS}date-processor.html`, + dateIndexName: `${ELASTICSEARCH_DOCS}date-index-name-processor.html`, + dissect: `${ELASTICSEARCH_DOCS}dissect-processor.html`, + dissectKeyModifiers: `${ELASTICSEARCH_DOCS}dissect-processor.html#dissect-key-modifiers`, + dotExpander: `${ELASTICSEARCH_DOCS}dot-expand-processor.html`, + drop: `${ELASTICSEARCH_DOCS}drop-processor.html`, + enrich: `${ELASTICSEARCH_DOCS}ingest-enriching-data.html`, + fail: `${ELASTICSEARCH_DOCS}fail-processor.html`, + foreach: `${ELASTICSEARCH_DOCS}foreach-processor.html`, + geoIp: `${ELASTICSEARCH_DOCS}geoip-processor.html`, + grok: `${ELASTICSEARCH_DOCS}grok-processor.html`, + gsub: `${ELASTICSEARCH_DOCS}gsub-processor.html`, + htmlString: `${ELASTICSEARCH_DOCS}htmlstrip-processor.html`, + inference: `${ELASTICSEARCH_DOCS}inference-processor.html`, + inferenceClassification: `${ELASTICSEARCH_DOCS}inference-processor.html#inference-processor-classification-opt`, + inferenceRegression: `${ELASTICSEARCH_DOCS}inference-processor.html#inference-processor-regression-opt`, + join: `${ELASTICSEARCH_DOCS}join-processor.html`, + json: `${ELASTICSEARCH_DOCS}json-processor.html`, + kv: `${ELASTICSEARCH_DOCS}kv-processor.html`, + lowercase: `${ELASTICSEARCH_DOCS}lowercase-processor.html`, + pipeline: `${ELASTICSEARCH_DOCS}pipeline-processor.html`, pipelines: `${ELASTICSEARCH_DOCS}ingest.html`, pipelineFailure: `${ELASTICSEARCH_DOCS}ingest.html#handling-pipeline-failures`, processors: `${ELASTICSEARCH_DOCS}processors.html`, + remove: `${ELASTICSEARCH_DOCS}remove-processor.html`, + rename: `${ELASTICSEARCH_DOCS}rename-processor.html`, + script: `${ELASTICSEARCH_DOCS}script-processor.html`, + set: `${ELASTICSEARCH_DOCS}set-processor.html`, + setSecurityUser: `${ELASTICSEARCH_DOCS}ingest-node-set-security-user-processor.html`, + sort: `${ELASTICSEARCH_DOCS}sort-processor.html`, + split: `${ELASTICSEARCH_DOCS}split-processor.html`, + trim: `${ELASTICSEARCH_DOCS}trim-processor.html`, + uppercase: `${ELASTICSEARCH_DOCS}uppercase-processor.html`, + uriParts: `${ELASTICSEARCH_DOCS}uri-parts-processor.html`, + urlDecode: `${ELASTICSEARCH_DOCS}urldecode-processor.html`, + userAgent: `${ELASTICSEARCH_DOCS}user-agent-processor.html`, }, }, }); @@ -446,6 +486,7 @@ export interface DocLinksStart { putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putWatch: string; + simulatePipeline: string; updateTransform: string; }>; readonly observability: Record; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0a1c7a9b0fa36..8327428991e13 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -627,6 +627,7 @@ export interface DocLinksStart { putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putWatch: string; + simulatePipeline: string; updateTransform: string; }>; readonly observability: Record; diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts index 5cb2a88c4733f..2fc78fc619cab 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -414,11 +414,18 @@ describe('ElasticIndex', () => { size: 100, query: { bool: { - must_not: { - term: { - type: 'fleet-agent-events', + must_not: [ + { + term: { + type: 'fleet-agent-events', + }, }, - }, + { + term: { + type: 'tsvb-validation-telemetry', + }, + }, + ], }, }, }, diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index a5f3cb36e736b..462425ff6e3e0 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -70,16 +70,19 @@ export function reader( let scrollId: string | undefined; // When migrating from the outdated index we use a read query which excludes - // saved objects which are no longer used. These saved objects will still be - // kept in the outdated index for backup purposes, but won't be availble in - // the upgraded index. - const excludeUnusedTypes = { + // saved object types which are no longer used. These saved objects will + // still be kept in the outdated index for backup purposes, but won't be + // availble in the upgraded index. + const EXCLUDE_UNUSED_TYPES = [ + 'fleet-agent-events', // https://github.com/elastic/kibana/issues/91869 + 'tsvb-validation-telemetry', // https://github.com/elastic/kibana/issues/95617 + ]; + + const excludeUnusedTypesQuery = { bool: { - must_not: { - term: { - type: 'fleet-agent-events', // https://github.com/elastic/kibana/issues/91869 - }, - }, + must_not: EXCLUDE_UNUSED_TYPES.map((type) => ({ + term: { type }, + })), }, }; @@ -92,7 +95,7 @@ export function reader( : client.search>({ body: { size: batchSize, - query: excludeUnusedTypes, + query: excludeUnusedTypesQuery, }, index, scroll, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 40d18c3b5063a..221e78e3e12e2 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -321,7 +321,7 @@ describe('KibanaMigrator', () => { options.client.tasks.get.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ completed: true, - error: { type: 'elatsicsearch_exception', reason: 'task failed with an error' }, + error: { type: 'elasticsearch_exception', reason: 'task failed with an error' }, failures: [], task: { description: 'task description' } as any, }) @@ -331,11 +331,11 @@ describe('KibanaMigrator', () => { migrator.prepareMigrations(); await expect(migrator.runMigrations()).rejects.toMatchInlineSnapshot(` [Error: Unable to complete saved object migrations for the [.my-index] index. Error: Reindex failed with the following error: - {"_tag":"Some","value":{"type":"elatsicsearch_exception","reason":"task failed with an error"}}] + {"_tag":"Some","value":{"type":"elasticsearch_exception","reason":"task failed with an error"}}] `); expect(loggingSystemMock.collect(options.logger).error[0][0]).toMatchInlineSnapshot(` [Error: Reindex failed with the following error: - {"_tag":"Some","value":{"type":"elatsicsearch_exception","reason":"task failed with an error"}}] + {"_tag":"Some","value":{"type":"elasticsearch_exception","reason":"task failed with an error"}}] `); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index 14ca73e7fcca0..bee17f42d7bdb 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -85,7 +85,8 @@ describe('actions', () => { 'my_source_index', 'my_target_index', Option.none, - false + false, + Option.none ); try { await task(); diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 8ac683a29d657..d759c0c9be20e 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -14,6 +14,7 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; import type { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; import { pipe } from 'fp-ts/lib/pipeable'; import { flow } from 'fp-ts/lib/function'; +import { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl'; import { ElasticsearchClient } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; import { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; @@ -436,7 +437,12 @@ export const reindex = ( sourceIndex: string, targetIndex: string, reindexScript: Option.Option, - requireAlias: boolean + requireAlias: boolean, + /* When reindexing we use a source query to exclude saved objects types which + * are no longer used. These saved objects will still be kept in the outdated + * index for backup purposes, but won't be availble in the upgraded index. + */ + unusedTypesToExclude: Option.Option ): TaskEither.TaskEither => () => { return client .reindex({ @@ -450,6 +456,15 @@ export const reindex = ( index: sourceIndex, // Set reindex batch size size: BATCH_SIZE, + // Exclude saved object types + query: Option.fold( + () => undefined, + (types) => ({ + bool: { + must_not: types.map((type) => ({ term: { type } })), + }, + }) + )(unusedTypesToExclude), }, dest: { index: targetIndex, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index aa9a5ea92ac11..3ed3ace416990 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -66,7 +66,8 @@ describe('migration actions', () => { { _source: { title: 'doc 1' } }, { _source: { title: 'doc 2' } }, { _source: { title: 'doc 3' } }, - { _source: { title: 'saved object 4' } }, + { _source: { title: 'saved object 4', type: 'another_unused_type' } }, + { _source: { title: 'f-agent-event 5', type: 'f_agent_event' } }, ] as unknown) as SavedObjectsRawDoc[]; await bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', sourceDocs)(); @@ -343,7 +344,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target', Option.none, - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -364,6 +366,37 @@ describe('migration actions', () => { "doc 2", "doc 3", "saved object 4", + "f-agent-event 5", + ] + `); + }); + it('resolves right and excludes all unusedTypesToExclude documents', async () => { + const res = (await reindex( + client, + 'existing_index_with_docs', + 'reindex_target_excluded_docs', + Option.none, + false, + Option.some(['f_agent_event', 'another_unused_type']) + )()) as Either.Right; + const task = waitForReindexTask(client, res.right.taskId, '10s'); + await expect(task()).resolves.toMatchInlineSnapshot(` + Object { + "_tag": "Right", + "right": "reindex_succeeded", + } + `); + + const results = ((await searchForOutdatedDocuments(client, { + batchSize: 1000, + targetIndex: 'reindex_target_excluded_docs', + outdatedDocumentsQuery: undefined, + })()) as Either.Right).right.outdatedDocuments; + expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` + Array [ + "doc 1", + "doc 2", + "doc 3", ] `); }); @@ -374,7 +407,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target_2', Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -394,6 +428,7 @@ describe('migration actions', () => { "doc 2_updated", "doc 3_updated", "saved object 4_updated", + "f-agent-event 5_updated", ] `); }); @@ -405,7 +440,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target_3', Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false + false, + Option.none )()) as Either.Right; let task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -421,7 +457,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target_3', Option.none, - false + false, + Option.none )()) as Either.Right; task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -443,6 +480,7 @@ describe('migration actions', () => { "doc 2_updated", "doc 3_updated", "saved object 4_updated", + "f-agent-event 5_updated", ] `); }); @@ -469,7 +507,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target_4', Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -491,6 +530,7 @@ describe('migration actions', () => { "doc 2", "doc 3_updated", "saved object 4_updated", + "f-agent-event 5_updated", ] `); }); @@ -517,7 +557,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target_5', Option.none, - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, reindexTaskId, '10s'); @@ -551,7 +592,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target_6', Option.none, - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, reindexTaskId, '10s'); @@ -571,7 +613,8 @@ describe('migration actions', () => { 'no_such_index', 'reindex_target', Option.none, - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -591,7 +634,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'existing_index_with_write_block', Option.none, - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); @@ -612,7 +656,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'existing_index_with_write_block', Option.none, - true + true, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); @@ -633,7 +678,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target', Option.none, - false + false, + Option.none )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '0s'); @@ -659,7 +705,8 @@ describe('migration actions', () => { 'existing_index_with_docs', 'reindex_target_7', Option.none, - false + false, + Option.none )()) as Either.Right; await waitForReindexTask(client, res.right.taskId, '10s')(); @@ -714,7 +761,7 @@ describe('migration actions', () => { targetIndex: 'existing_index_with_docs', outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - expect(resultsWithoutQuery.length).toBe(4); + expect(resultsWithoutQuery.length).toBe(5); }); it('resolves with _id, _source, _seq_no and _primary_term', async () => { expect.assertions(1); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index d4ce7b74baa5f..2c2cd0032abfd 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -249,6 +249,13 @@ describe('migrationsStateActionMachine', () => { }, }, }, + "unusedTypesToExclude": Object { + "_tag": "Some", + "value": Array [ + "fleet-agent-events", + "tsvb-validation-telemetry", + ], + }, "versionAlias": ".my-so-index_7.11.0", "versionIndex": ".my-so-index_7.11.0_001", }, @@ -310,6 +317,13 @@ describe('migrationsStateActionMachine', () => { }, }, }, + "unusedTypesToExclude": Object { + "_tag": "Some", + "value": Array [ + "fleet-agent-events", + "tsvb-validation-telemetry", + ], + }, "versionAlias": ".my-so-index_7.11.0", "versionIndex": ".my-so-index_7.11.0_001", }, @@ -456,6 +470,13 @@ describe('migrationsStateActionMachine', () => { }, }, }, + "unusedTypesToExclude": Object { + "_tag": "Some", + "value": Array [ + "fleet-agent-events", + "tsvb-validation-telemetry", + ], + }, "versionAlias": ".my-so-index_7.11.0", "versionIndex": ".my-so-index_7.11.0_001", }, @@ -512,6 +533,13 @@ describe('migrationsStateActionMachine', () => { }, }, }, + "unusedTypesToExclude": Object { + "_tag": "Some", + "value": Array [ + "fleet-agent-events", + "tsvb-validation-telemetry", + ], + }, "versionAlias": ".my-so-index_7.11.0", "versionIndex": ".my-so-index_7.11.0_001", }, diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index f9bf3418c0ab6..4fd9b7cbb3df4 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -69,6 +69,7 @@ describe('migrations v2 model', () => { versionAlias: '.kibana_7.11.0', versionIndex: '.kibana_7.11.0_001', tempIndex: '.kibana_7.11.0_reindex_temp', + unusedTypesToExclude: Option.some(['unused-fleet-agent-events']), }; describe('exponential retry delays for retryable_es_client_error', () => { @@ -1242,6 +1243,13 @@ describe('migrations v2 model', () => { }, }, }, + "unusedTypesToExclude": Object { + "_tag": "Some", + "value": Array [ + "fleet-agent-events", + "tsvb-validation-telemetry", + ], + }, "versionAlias": ".kibana_task_manager_8.1.0", "versionIndex": ".kibana_task_manager_8.1.0_001", } diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index e62bd108faea0..2353452a6a51b 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -768,6 +768,11 @@ export const createInitialState = ({ }, }; + const unusedTypesToExclude = Option.some([ + 'fleet-agent-events', // https://github.com/elastic/kibana/issues/91869 + 'tsvb-validation-telemetry', // https://github.com/elastic/kibana/issues/95617 + ]); + const initialState: InitState = { controlState: 'INIT', indexPrefix, @@ -786,6 +791,7 @@ export const createInitialState = ({ retryAttempts: migrationsConfig.retryAttempts, batchSize: migrationsConfig.batchSize, logs: [], + unusedTypesToExclude, }; return initialState; }; diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 5c159f4f24e22..67b2004a4b31a 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -61,7 +61,14 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra CREATE_REINDEX_TEMP: (state: CreateReindexTempState) => Actions.createIndex(client, state.tempIndex, state.tempIndexMappings), REINDEX_SOURCE_TO_TEMP: (state: ReindexSourceToTempState) => - Actions.reindex(client, state.sourceIndex.value, state.tempIndex, Option.none, false), + Actions.reindex( + client, + state.sourceIndex.value, + state.tempIndex, + Option.none, + false, + state.unusedTypesToExclude + ), SET_TEMP_WRITE_BLOCK: (state: SetTempWriteBlock) => Actions.setWriteBlock(client, state.tempIndex), REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK: (state: ReindexSourceToTempWaitForTaskState) => @@ -104,7 +111,8 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra state.legacyIndex, state.sourceIndex.value, state.preMigrationScript, - false + false, + state.unusedTypesToExclude ), LEGACY_REINDEX_WAIT_FOR_TASK: (state: LegacyReindexWaitForTaskState) => Actions.waitForReindexTask(client, state.legacyReindexTaskId, '60s'), diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index 8d6fe3f030eb3..cc4aa18171843 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -89,6 +89,11 @@ export interface BaseState extends ControlState { * prevents lost deletes e.g. `.kibana_7.11.0_reindex`. */ readonly tempIndex: string; + /* When reindexing we use a source query to exclude saved objects types which + * are no longer used. These saved objects will still be kept in the outdated + * index for backup purposes, but won't be availble in the upgraded index. + */ + readonly unusedTypesToExclude: Option.Option; } export type InitState = BaseState & { diff --git a/src/plugins/dashboard/common/bwc/types.ts b/src/plugins/dashboard/common/bwc/types.ts index c296c0a65f165..f3c384a76c391 100644 --- a/src/plugins/dashboard/common/bwc/types.ts +++ b/src/plugins/dashboard/common/bwc/types.ts @@ -78,6 +78,7 @@ export type RawSavedDashboardPanel730ToLatest = Pick< readonly name?: string; panelIndex: string; + panelRefName?: string; }; // NOTE!! diff --git a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.test.ts b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.test.ts new file mode 100644 index 0000000000000..20b2c12ab4b23 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.test.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createExtract, createInject } from './dashboard_container_persistable_state'; +import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks'; +import { DashboardContainerStateWithType } from '../types'; + +const persistableStateService = createEmbeddablePersistableStateServiceMock(); + +const dashboardWithExtractedPanel: DashboardContainerStateWithType = { + id: 'id', + type: 'dashboard', + panels: { + panel_1: { + type: 'panel_type', + gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, + panelRefName: 'panel_panel_1', + explicitInput: { + id: 'panel_1', + }, + }, + }, +}; + +const extractedSavedObjectPanelRef = { + name: 'panel_1:panel_panel_1', + type: 'panel_type', + id: 'object-id', +}; + +const unextractedDashboardState: DashboardContainerStateWithType = { + id: 'id', + type: 'dashboard', + panels: { + panel_1: { + type: 'panel_type', + gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, + explicitInput: { + id: 'panel_1', + savedObjectId: 'object-id', + }, + }, + }, +}; + +describe('inject/extract by reference panel', () => { + it('should inject the extracted saved object panel', () => { + const inject = createInject(persistableStateService); + const references = [extractedSavedObjectPanelRef]; + + const injected = inject( + dashboardWithExtractedPanel, + references + ) as DashboardContainerStateWithType; + + expect(injected).toEqual(unextractedDashboardState); + }); + + it('should extract the saved object panel', () => { + const extract = createExtract(persistableStateService); + const { state: extractedState, references: extractedReferences } = extract( + unextractedDashboardState + ); + + expect(extractedState).toEqual(dashboardWithExtractedPanel); + expect(extractedReferences[0]).toEqual(extractedSavedObjectPanelRef); + }); +}); + +const dashboardWithExtractedByValuePanel: DashboardContainerStateWithType = { + id: 'id', + type: 'dashboard', + panels: { + panel_1: { + type: 'panel_type', + gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, + explicitInput: { + id: 'panel_1', + extracted_reference: 'ref', + }, + }, + }, +}; + +const extractedByValueRef = { + id: 'id', + name: 'panel_1:ref', + type: 'panel_type', +}; + +const unextractedDashboardByValueState: DashboardContainerStateWithType = { + id: 'id', + type: 'dashboard', + panels: { + panel_1: { + type: 'panel_type', + gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, + explicitInput: { + id: 'panel_1', + value: 'id', + }, + }, + }, +}; + +describe('inject/extract by value panels', () => { + it('should inject the extracted references', () => { + const inject = createInject(persistableStateService); + + persistableStateService.inject.mockImplementationOnce((state, references) => { + const ref = references.find((r) => r.name === 'ref'); + if (!ref) { + return state; + } + + if (('extracted_reference' in state) as any) { + (state as any).value = ref.id; + delete (state as any).extracted_reference; + } + + return state; + }); + + const injectedState = inject(dashboardWithExtractedByValuePanel, [extractedByValueRef]); + + expect(injectedState).toEqual(unextractedDashboardByValueState); + }); + + it('should extract references using persistable state', () => { + const extract = createExtract(persistableStateService); + + persistableStateService.extract.mockImplementationOnce((state) => { + if ((state as any).value === 'id') { + delete (state as any).value; + (state as any).extracted_reference = 'ref'; + + return { + state, + references: [{ id: extractedByValueRef.id, name: 'ref', type: extractedByValueRef.type }], + }; + } + + return { state, references: [] }; + }); + + const { state: extractedState, references: extractedReferences } = extract( + unextractedDashboardByValueState + ); + + expect(extractedState).toEqual(dashboardWithExtractedByValuePanel); + expect(extractedReferences).toEqual([extractedByValueRef]); + }); +}); diff --git a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts new file mode 100644 index 0000000000000..6104fcfdbe949 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EmbeddableInput, + EmbeddablePersistableStateService, + EmbeddableStateWithType, +} from '../../../embeddable/common'; +import { SavedObjectReference } from '../../../../core/types'; +import { DashboardContainerStateWithType, DashboardPanelState } from '../types'; + +const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`; + +export const createInject = ( + persistableStateService: EmbeddablePersistableStateService +): EmbeddablePersistableStateService['inject'] => { + return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => { + const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType; + + if ('panels' in workingState) { + workingState.panels = { ...workingState.panels }; + + for (const [key, panel] of Object.entries(workingState.panels)) { + workingState.panels[key] = { ...panel }; + // Find the references for this panel + const prefix = getPanelStatePrefix(panel); + + const filteredReferences = references + .filter((reference) => reference.name.indexOf(prefix) === 0) + .map((reference) => ({ ...reference, name: reference.name.replace(prefix, '') })); + + const panelReferences = filteredReferences.length === 0 ? references : filteredReferences; + + // Inject dashboard references back in + if (panel.panelRefName !== undefined) { + const matchingReference = panelReferences.find( + (reference) => reference.name === panel.panelRefName + ); + + if (!matchingReference) { + throw new Error(`Could not find reference "${panel.panelRefName}"`); + } + + if (matchingReference !== undefined) { + workingState.panels[key] = { + ...panel, + type: matchingReference.type, + explicitInput: { + ...workingState.panels[key].explicitInput, + savedObjectId: matchingReference.id, + }, + }; + + delete workingState.panels[key].panelRefName; + } + } + + const { type, ...injectedState } = persistableStateService.inject( + { ...workingState.panels[key].explicitInput, type: workingState.panels[key].type }, + panelReferences + ); + + workingState.panels[key].explicitInput = injectedState as EmbeddableInput; + } + } + + return workingState as EmbeddableStateWithType; + }; +}; + +export const createExtract = ( + persistableStateService: EmbeddablePersistableStateService +): EmbeddablePersistableStateService['extract'] => { + return (state: EmbeddableStateWithType) => { + const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType; + + const references: SavedObjectReference[] = []; + + if ('panels' in workingState) { + workingState.panels = { ...workingState.panels }; + + // Run every panel through the state service to get the nested references + for (const [key, panel] of Object.entries(workingState.panels)) { + const prefix = getPanelStatePrefix(panel); + + // If the panel is a saved object, then we will make the reference for that saved object and change the explicit input + if (panel.explicitInput.savedObjectId) { + panel.panelRefName = `panel_${key}`; + + references.push({ + name: `${prefix}panel_${key}`, + type: panel.type, + id: panel.explicitInput.savedObjectId as string, + }); + + delete panel.explicitInput.savedObjectId; + delete panel.explicitInput.type; + } + + const { state: panelState, references: panelReferences } = persistableStateService.extract({ + ...panel.explicitInput, + type: panel.type, + }); + + // We're going to prefix the names of the references so that we don't end up with dupes (from visualizations for instance) + const prefixedReferences = panelReferences.map((reference) => ({ + ...reference, + name: `${prefix}${reference.name}`, + })); + + references.push(...prefixedReferences); + + const { type, ...restOfState } = panelState; + workingState.panels[key].explicitInput = restOfState as EmbeddableInput; + } + } + + return { state: workingState as EmbeddableStateWithType, references }; + }; +}; diff --git a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index c67cd325572ff..6fe1888c6f4ae 100644 --- a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -16,6 +16,7 @@ export function convertSavedDashboardPanelToPanelState( return { type: savedDashboardPanel.type, gridData: savedDashboardPanel.gridData, + panelRefName: savedDashboardPanel.panelRefName, explicitInput: { id: savedDashboardPanel.panelIndex, ...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }), @@ -41,5 +42,6 @@ export function convertPanelStateToSavedDashboardPanel( embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), ...(customTitle && { title: customTitle }), ...(savedObjectId !== undefined && { id: savedObjectId }), + ...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }), }; } diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts index a1d5487eeb244..017b7d804c872 100644 --- a/src/plugins/dashboard/common/index.ts +++ b/src/plugins/dashboard/common/index.ts @@ -14,6 +14,7 @@ export { DashboardDocPre700, } from './bwc/types'; export { + DashboardContainerStateWithType, SavedDashboardPanelTo60, SavedDashboardPanel610, SavedDashboardPanel620, diff --git a/src/plugins/dashboard/common/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/saved_dashboard_references.test.ts index 584d7e5e63a92..9ab0e7b644496 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.test.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.test.ts @@ -12,14 +12,34 @@ import { InjectDeps, ExtractDeps, } from './saved_dashboard_references'; + +import { createExtract, createInject } from './embeddable/dashboard_container_persistable_state'; import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks'; const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); +const dashboardInject = createInject(embeddablePersistableStateServiceMock); +const dashboardExtract = createExtract(embeddablePersistableStateServiceMock); + +embeddablePersistableStateServiceMock.extract.mockImplementation((state) => { + if (state.type === 'dashboard') { + return dashboardExtract(state); + } + + return { state, references: [] }; +}); + +embeddablePersistableStateServiceMock.inject.mockImplementation((state, references) => { + if (state.type === 'dashboard') { + return dashboardInject(state, references); + } + + return state; +}); const deps: InjectDeps & ExtractDeps = { embeddablePersistableStateService: embeddablePersistableStateServiceMock, }; -describe('extractReferences', () => { +describe('legacy extract references', () => { test('extracts references from panelsJSON', () => { const doc = { id: '1', @@ -30,13 +50,13 @@ describe('extractReferences', () => { type: 'visualization', id: '1', title: 'Title 1', - version: '7.9.1', + version: '7.0.0', }, { type: 'visualization', id: '2', title: 'Title 2', - version: '7.9.1', + version: '7.0.0', }, ]), }, @@ -48,7 +68,7 @@ describe('extractReferences', () => { Object { "attributes": Object { "foo": true, - "panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"version\\":\\"7.9.1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", + "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_1\\"}]", }, "references": Array [ Object { @@ -75,7 +95,7 @@ describe('extractReferences', () => { { id: '1', title: 'Title 1', - version: '7.9.1', + version: '7.0.0', }, ]), }, @@ -186,6 +206,102 @@ describe('extractReferences', () => { }); }); +describe('extractReferences', () => { + test('extracts references from panelsJSON', () => { + const doc = { + id: '1', + attributes: { + foo: true, + panelsJSON: JSON.stringify([ + { + panelIndex: 'panel-1', + type: 'visualization', + id: '1', + title: 'Title 1', + version: '7.9.1', + }, + { + panelIndex: 'panel-2', + type: 'visualization', + id: '2', + title: 'Title 2', + version: '7.9.1', + }, + ]), + }, + references: [], + }; + const updatedDoc = extractReferences(doc, deps); + + expect(updatedDoc).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_panel-1\\"},{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-2\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_panel-2\\"}]", + }, + "references": Array [ + Object { + "id": "1", + "name": "panel-1:panel_panel-1", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel-2:panel_panel-2", + "type": "visualization", + }, + ], + } + `); + }); + + test('fails when "type" attribute is missing from a panel', () => { + const doc = { + id: '1', + attributes: { + foo: true, + panelsJSON: JSON.stringify([ + { + id: '1', + title: 'Title 1', + version: '7.9.1', + }, + ]), + }, + references: [], + }; + expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot( + `"\\"type\\" attribute is missing from panel \\"0\\""` + ); + }); + + test('passes when "id" attribute is missing from a panel', () => { + const doc = { + id: '1', + attributes: { + foo: true, + panelsJSON: JSON.stringify([ + { + type: 'visualization', + title: 'Title 1', + version: '7.9.1', + }, + ]), + }, + references: [], + }; + expect(extractReferences(doc, deps)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]", + }, + "references": Array [], + } + `); + }); +}); + describe('injectReferences', () => { test('returns injected attributes', () => { const attributes = { @@ -195,10 +311,12 @@ describe('injectReferences', () => { { panelRefName: 'panel_0', title: 'Title 1', + version: '7.9.0', }, { panelRefName: 'panel_1', title: 'Title 2', + version: '7.9.0', }, ]), }; @@ -219,7 +337,7 @@ describe('injectReferences', () => { expect(newAttributes).toMatchInlineSnapshot(` Object { "id": "1", - "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]", + "panelsJSON": "[{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]", "title": "test", } `); @@ -280,7 +398,7 @@ describe('injectReferences', () => { expect(newAttributes).toMatchInlineSnapshot(` Object { "id": "1", - "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]", + "panelsJSON": "[{\\"version\\":\\"\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]", "title": "test", } `); diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts index f1fea99057f83..16ab470ce7d6f 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -8,22 +8,71 @@ import semverSatisfies from 'semver/functions/satisfies'; import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; -import { - extractPanelsReferences, - injectPanelsReferences, -} from './embeddable/embeddable_references'; -import { SavedDashboardPanel730ToLatest } from './types'; +import { DashboardContainerStateWithType, DashboardPanelState } from './types'; import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; - +import { + convertPanelStateToSavedDashboardPanel, + convertSavedDashboardPanelToPanelState, +} from './embeddable/embeddable_saved_object_converters'; +import { SavedDashboardPanel } from './types'; export interface ExtractDeps { embeddablePersistableStateService: EmbeddablePersistableStateService; } - export interface SavedObjectAttributesAndReferences { attributes: SavedObjectAttributes; references: SavedObjectReference[]; } +const isPre730Panel = (panel: Record): boolean => { + return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true; +}; + +function dashboardAttributesToState( + attributes: SavedObjectAttributes +): { + state: DashboardContainerStateWithType; + panels: SavedDashboardPanel[]; +} { + let inputPanels = [] as SavedDashboardPanel[]; + if (typeof attributes.panelsJSON === 'string') { + inputPanels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[]; + } + + return { + panels: inputPanels, + state: { + id: attributes.id as string, + type: 'dashboard', + panels: inputPanels.reduce>((current, panel, index) => { + const panelIndex = panel.panelIndex || `${index}`; + current[panelIndex] = convertSavedDashboardPanelToPanelState(panel); + return current; + }, {}), + }, + }; +} + +function panelStatesToPanels( + panelStates: DashboardContainerStateWithType['panels'], + originalPanels: SavedDashboardPanel[] +): SavedDashboardPanel[] { + return Object.entries(panelStates).map(([id, panelState]) => { + // Find matching original panel to get the version + let originalPanel = originalPanels.find((p) => p.panelIndex === id); + + if (!originalPanel) { + // Maybe original panel doesn't have a panel index and it's just straight up based on it's index + const numericId = parseInt(id, 10); + originalPanel = isNaN(numericId) ? originalPanel : originalPanels[numericId]; + } + + return convertPanelStateToSavedDashboardPanel( + panelState, + originalPanel?.version ? originalPanel.version : '' + ); + }); +} + export function extractReferences( { attributes, references = [] }: SavedObjectAttributesAndReferences, deps: ExtractDeps @@ -31,64 +80,33 @@ export function extractReferences( if (typeof attributes.panelsJSON !== 'string') { return { attributes, references }; } - const panelReferences: SavedObjectReference[] = []; - let panels: Array> = JSON.parse(String(attributes.panelsJSON)); - const isPre730Panel = (panel: Record): boolean => { - return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true; - }; + const { panels, state } = dashboardAttributesToState(attributes); - const hasPre730Panel = panels.some(isPre730Panel); - - /** - * `extractPanelsReferences` only knows how to reliably handle "latest" panels - * It is possible that `extractReferences` is run on older dashboard SO with older panels, - * for example, when importing a saved object using saved object UI `extractReferences` is called BEFORE any server side migrations are run. - * - * In this case we skip running `extractPanelsReferences` on such object. - * We also know that there is nothing to extract - * (First possible entity to be extracted by this mechanism is a dashboard drilldown since 7.11) - */ - if (!hasPre730Panel) { - const extractedReferencesResult = extractPanelsReferences( - // it is ~safe~ to cast to `SavedDashboardPanel730ToLatest` because above we've checked that there are only >=7.3 panels - (panels as unknown) as SavedDashboardPanel730ToLatest[], - deps - ); + if (((panels as unknown) as Array>).some(isPre730Panel)) { + return pre730ExtractReferences({ attributes, references }, deps); + } - panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array< - Record - >; - extractedReferencesResult.forEach((res) => { - panelReferences.push(...res.references); - }); + const missingTypeIndex = panels.findIndex((panel) => panel.type === undefined); + if (missingTypeIndex >= 0) { + throw new Error(`"type" attribute is missing from panel "${missingTypeIndex}"`); } - // TODO: This extraction should be done by EmbeddablePersistableStateService - // https://github.com/elastic/kibana/issues/82830 - panels.forEach((panel, i) => { - if (!panel.type) { - throw new Error(`"type" attribute is missing from panel "${i}"`); - } - if (!panel.id) { - // Embeddables are not required to be backed off a saved object. - return; - } - panel.panelRefName = `panel_${i}`; - panelReferences.push({ - name: `panel_${i}`, - type: panel.type, - id: panel.id, - }); - delete panel.type; - delete panel.id; - }); + const { + state: extractedState, + references: extractedReferences, + } = deps.embeddablePersistableStateService.extract(state); + + const extractedPanels = panelStatesToPanels( + (extractedState as DashboardContainerStateWithType).panels, + panels + ); return { - references: [...references, ...panelReferences], + references: [...references, ...extractedReferences], attributes: { ...attributes, - panelsJSON: JSON.stringify(panels), + panelsJSON: JSON.stringify(extractedPanels), }, }; } @@ -107,33 +125,60 @@ export function injectReferences( if (typeof attributes.panelsJSON !== 'string') { return attributes; } - let panels = JSON.parse(attributes.panelsJSON); + const parsedPanels = JSON.parse(attributes.panelsJSON); // Same here, prevent failing saved object import if ever panels aren't an array. - if (!Array.isArray(panels)) { + if (!Array.isArray(parsedPanels)) { return attributes; } - // TODO: This injection should be done by EmbeddablePersistableStateService - // https://github.com/elastic/kibana/issues/82830 - panels.forEach((panel) => { - if (!panel.panelRefName) { - return; + const { panels, state } = dashboardAttributesToState(attributes); + + const injectedState = deps.embeddablePersistableStateService.inject(state, references); + const injectedPanels = panelStatesToPanels( + (injectedState as DashboardContainerStateWithType).panels, + panels + ); + + return { + ...attributes, + panelsJSON: JSON.stringify(injectedPanels), + }; +} + +function pre730ExtractReferences( + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: ExtractDeps +): SavedObjectAttributesAndReferences { + if (typeof attributes.panelsJSON !== 'string') { + return { attributes, references }; + } + const panelReferences: SavedObjectReference[] = []; + const panels: Array> = JSON.parse(String(attributes.panelsJSON)); + + panels.forEach((panel, i) => { + if (!panel.type) { + throw new Error(`"type" attribute is missing from panel "${i}"`); } - const reference = references.find((ref) => ref.name === panel.panelRefName); - if (!reference) { - // Throw an error since "panelRefName" means the reference exists within - // "references" and in this scenario we have bad data. - throw new Error(`Could not find reference "${panel.panelRefName}"`); + if (!panel.id) { + // Embeddables are not required to be backed off a saved object. + return; } - panel.id = reference.id; - panel.type = reference.type; - delete panel.panelRefName; - }); - panels = injectPanelsReferences(panels, references, deps); + panel.panelRefName = `panel_${i}`; + panelReferences.push({ + name: `panel_${i}`, + type: panel.type, + id: panel.id, + }); + delete panel.type; + delete panel.id; + }); return { - ...attributes, - panelsJSON: JSON.stringify(panels), + references: [...references, ...panelReferences], + attributes: { + ...attributes, + panelsJSON: JSON.stringify(panels), + }, }; } diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index c8ef3c81662c7..9a6d185ef2ac1 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types'; +import { + EmbeddableInput, + EmbeddableStateWithType, + PanelState, +} from '../../../../src/plugins/embeddable/common/types'; import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable'; import { RawSavedDashboardPanelTo60, @@ -25,6 +29,7 @@ export interface DashboardPanelState< TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput > extends PanelState { readonly gridData: GridData; + panelRefName?: string; } /** @@ -80,3 +85,11 @@ export type SavedDashboardPanel730ToLatest = Pick< readonly id?: string; readonly type: string; }; + +// Making this interface because so much of the Container type from embeddable is tied up in public +// Once that is all available from common, we should be able to move the dashboard_container type to our common as well +export interface DashboardContainerStateWithType extends EmbeddableStateWithType { + panels: { + [panelId: string]: DashboardPanelState; + }; +} diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx index 6501f92689d17..9b93f0bbd0711 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common'; import { Container, ErrorEmbeddable, @@ -20,6 +21,10 @@ import { DashboardContainerServices, } from './dashboard_container'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; +import { + createExtract, + createInject, +} from '../../../common/embeddable/dashboard_container_persistable_state'; export type DashboardContainerFactory = EmbeddableFactory< DashboardContainerInput, @@ -32,7 +37,10 @@ export class DashboardContainerFactoryDefinition public readonly isContainerType = true; public readonly type = DASHBOARD_CONTAINER_TYPE; - constructor(private readonly getStartServices: () => Promise) {} + constructor( + private readonly getStartServices: () => Promise, + private readonly persistableStateService: EmbeddablePersistableStateService + ) {} public isEditable = async () => { // Currently unused for dashboards @@ -62,4 +70,8 @@ export class DashboardContainerFactoryDefinition const services = await this.getStartServices(); return new DashboardContainer(initialInput, services, parent); }; + + public inject = createInject(this.persistableStateService); + + public extract = createExtract(this.persistableStateService); } diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 5bf730996ab4f..e2f52a47455b3 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -121,9 +121,11 @@ export type DashboardSetup = void; export interface DashboardStart { getSavedDashboardLoader: () => SavedObjectLoader; + getDashboardContainerByValueRenderer: () => ReturnType< + typeof createDashboardContainerByValueRenderer + >; dashboardUrlGenerator?: DashboardUrlGenerator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; - DashboardContainerByValueRenderer: ReturnType; } export class DashboardPlugin @@ -260,8 +262,16 @@ export class DashboardPlugin }, }); - const dashboardContainerFactory = new DashboardContainerFactoryDefinition(getStartServices); - embeddable.registerEmbeddableFactory(dashboardContainerFactory.type, dashboardContainerFactory); + getStartServices().then((coreStart) => { + const dashboardContainerFactory = new DashboardContainerFactoryDefinition( + getStartServices, + coreStart.embeddable + ); + embeddable.registerEmbeddableFactory( + dashboardContainerFactory.type, + dashboardContainerFactory + ); + }); const placeholderFactory = new PlaceholderEmbeddableFactory(); embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory); @@ -403,17 +413,24 @@ export class DashboardPlugin savedObjects: plugins.savedObjects, embeddableStart: plugins.embeddable, }); - const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( - DASHBOARD_CONTAINER_TYPE - )! as DashboardContainerFactory; return { getSavedDashboardLoader: () => savedDashboardLoader, + getDashboardContainerByValueRenderer: () => { + const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( + DASHBOARD_CONTAINER_TYPE + ); + + if (!dashboardContainerFactory) { + throw new Error(`${DASHBOARD_CONTAINER_TYPE} Embeddable Factory not found`); + } + + return createDashboardContainerByValueRenderer({ + factory: dashboardContainerFactory as DashboardContainerFactory, + }); + }, dashboardUrlGenerator: this.dashboardUrlGenerator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, - DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ - factory: dashboardContainerFactory, - }), }; } diff --git a/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts b/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts new file mode 100644 index 0000000000000..995731341739a --- /dev/null +++ b/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common'; +import { EmbeddableRegistryDefinition } from '../../../embeddable/server'; +import { + createExtract, + createInject, +} from '../../common/embeddable/dashboard_container_persistable_state'; + +export const dashboardPersistableStateServiceFactory = ( + persistableStateService: EmbeddablePersistableStateService +): EmbeddableRegistryDefinition => { + return { + id: 'dashboard', + extract: createExtract(persistableStateService), + inject: createInject(persistableStateService), + }; +}; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index 020ecfeaa9239..3aeaf31c190bd 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -18,24 +18,29 @@ import { createDashboardSavedObjectType } from './saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; -import { EmbeddableSetup } from '../../embeddable/server'; +import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/server'; import { UsageCollectionSetup } from '../../usage_collection/server'; import { registerDashboardUsageCollector } from './usage/register_collector'; +import { dashboardPersistableStateServiceFactory } from './embeddable/dashboard_container_embeddable_factory'; interface SetupDeps { embeddable: EmbeddableSetup; usageCollection: UsageCollectionSetup; } +interface StartDeps { + embeddable: EmbeddableStart; +} + export class DashboardPlugin - implements Plugin { + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup, plugins: SetupDeps) { + public setup(core: CoreSetup, plugins: SetupDeps) { this.logger.debug('dashboard: Setup'); core.savedObjects.registerType( @@ -48,6 +53,15 @@ export class DashboardPlugin core.capabilities.registerProvider(capabilitiesProvider); registerDashboardUsageCollector(plugins.usageCollection, plugins.embeddable); + + (async () => { + const [, startPlugins] = await core.getStartServices(); + + plugins.embeddable.registerEmbeddableFactory( + dashboardPersistableStateServiceFactory(startPlugins.embeddable) + ); + })(); + return {}; } diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index e2949847bc926..9671a8d847c0a 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -6,13 +6,39 @@ * Side Public License, v 1. */ -import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { SavedObjectReference, SavedObjectUnsanitizedDoc } from 'kibana/server'; import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; import { DashboardDoc730ToLatest } from '../../common'; +import { + createExtract, + createInject, +} from '../../common/embeddable/dashboard_container_persistable_state'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; const embeddableSetupMock = createEmbeddableSetupMock(); +const extract = createExtract(embeddableSetupMock); +const inject = createInject(embeddableSetupMock); +const extractImplementation = (state: EmbeddableStateWithType) => { + if (state.type === 'dashboard') { + return extract(state); + } + return { state, references: [] }; +}; +const injectImplementation = ( + state: EmbeddableStateWithType, + references: SavedObjectReference[] +) => { + if (state.type === 'dashboard') { + return inject(state, references); + } + + return state; +}; +embeddableSetupMock.extract.mockImplementation(extractImplementation); +embeddableSetupMock.inject.mockImplementation(injectImplementation); + const migrations = createDashboardSavedObjectTypeMigrations({ embeddable: embeddableSetupMock, }); @@ -25,10 +51,10 @@ describe('dashboard', () => { test('skips error on empty object', () => { expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(` -Object { - "references": Array [], -} -`); + Object { + "references": Array [], + } + `); }); test('skips errors when searchSourceJSON is null', () => { @@ -45,29 +71,29 @@ Object { }; const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('skips errors when searchSourceJSON is undefined', () => { @@ -84,29 +110,29 @@ Object { }; const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('skips error when searchSourceJSON is not a string', () => { @@ -122,29 +148,29 @@ Object { }, }; expect(migration(doc, contextMock)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('skips error when searchSourceJSON is invalid json', () => { @@ -160,29 +186,29 @@ Object { }, }; expect(migration(doc, contextMock)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { @@ -199,29 +225,29 @@ Object { }; const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('extracts "index" attribute from doc', () => { @@ -238,34 +264,34 @@ Object { }; const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('extracts index patterns from filter', () => { @@ -293,34 +319,34 @@ Object { const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('skips error when panelsJSON is not a string', () => { @@ -331,14 +357,14 @@ Object { }, } as SavedObjectUnsanitizedDoc; expect(migration(doc, contextMock)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": 123, - }, - "id": "1", - "references": Array [], -} -`); + Object { + "attributes": Object { + "panelsJSON": 123, + }, + "id": "1", + "references": Array [], + } + `); }); test('skips error when panelsJSON is not valid JSON', () => { @@ -349,14 +375,14 @@ Object { }, } as SavedObjectUnsanitizedDoc; expect(migration(doc, contextMock)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "{123abc}", - }, - "id": "1", - "references": Array [], -} -`); + Object { + "attributes": Object { + "panelsJSON": "{123abc}", + }, + "id": "1", + "references": Array [], + } + `); }); test('skips panelsJSON when its not an array', () => { @@ -367,14 +393,14 @@ Object { }, } as SavedObjectUnsanitizedDoc; expect(migration(doc, contextMock)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "{}", - }, - "id": "1", - "references": Array [], -} -`); + Object { + "attributes": Object { + "panelsJSON": "{}", + }, + "id": "1", + "references": Array [], + } + `); }); test('skips error when a panel is missing "type" attribute', () => { @@ -385,14 +411,14 @@ Object { }, } as SavedObjectUnsanitizedDoc; expect(migration(doc, contextMock)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"id\\":\\"123\\"}]", - }, - "id": "1", - "references": Array [], -} -`); + Object { + "attributes": Object { + "panelsJSON": "[{\\"id\\":\\"123\\"}]", + }, + "id": "1", + "references": Array [], + } + `); }); test('skips error when a panel is missing "id" attribute', () => { @@ -403,14 +429,14 @@ Object { }, } as SavedObjectUnsanitizedDoc; expect(migration(doc, contextMock)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", - }, - "id": "1", - "references": Array [], -} -`); + Object { + "attributes": Object { + "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", + }, + "id": "1", + "references": Array [], + } + `); }); test('extract panel references from doc', () => { @@ -423,25 +449,25 @@ Object { } as SavedObjectUnsanitizedDoc; const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); + Object { + "attributes": Object { + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + } + `); }); }); @@ -475,19 +501,57 @@ Object { test('should migrate 7.3.0 doc without embeddable state to extract', () => { const newDoc = migration(doc, contextMock); - expect(newDoc).toEqual(doc); + expect(newDoc).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "description": "", + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"\\"},\\"filter\\":[{\\"query\\":{\\"match_phrase\\":{\\"machine.os.keyword\\":\\"osx\\"}},\\"$state\\":{\\"store\\":\\"appState\\"},\\"meta\\":{\\"type\\":\\"phrase\\",\\"key\\":\\"machine.os.keyword\\",\\"params\\":{\\"query\\":\\"osx\\"},\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + "optionsJSON": "{\\"useMargins\\":true,\\"hidePanelTitles\\":false}", + "panelsJSON": "[{\\"version\\":\\"7.9.3\\",\\"type\\":\\"visualization\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\"},\\"panelIndex\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\",\\"embeddableConfig\\":{\\"enhancements\\":{\\"dynamicActions\\":{\\"events\\":[]}}},\\"panelRefName\\":\\"panel_82fa0882-9f9e-476a-bbb9-03555e5ced91\\"}]", + "timeRestore": false, + "title": "Dashboard A", + "version": 1, + }, + "id": "376e6260-1f5e-11eb-91aa-7b6d5f8a61d6", + "references": Array [ + Object { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + Object { + "id": "14e2e710-4258-11e8-b3aa-73fdaf54bfc9", + "name": "82fa0882-9f9e-476a-bbb9-03555e5ced91:panel_82fa0882-9f9e-476a-bbb9-03555e5ced91", + "type": "visualization", + }, + ], + "type": "dashboard", + } + `); }); test('should migrate 7.3.0 doc and extract embeddable state', () => { - embeddableSetupMock.extract.mockImplementationOnce((state) => ({ - state: { ...state, __extracted: true }, - references: [{ id: '__new', name: '__newRefName', type: '__newType' }], - })); + embeddableSetupMock.extract.mockImplementation((state) => { + const stateAndReferences = extractImplementation(state); + const { references } = stateAndReferences; + let { state: newState } = stateAndReferences; + + if (state.enhancements !== undefined && Object.keys(state.enhancements).length !== 0) { + newState = { ...state, __extracted: true } as any; + references.push({ id: '__new', name: '__newRefName', type: '__newType' }); + } + + return { state: newState, references }; + }); const newDoc = migration(doc, contextMock); expect(newDoc).not.toEqual(doc); expect(newDoc.references).toHaveLength(doc.references.length + 1); expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true); + + embeddableSetupMock.extract.mockImplementation(extractImplementation); }); }); }); diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index b8af6ad3a99e5..3dda97566da5a 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -59,7 +59,7 @@ export const setupValueSuggestionProvider = ( return core.http .fetch(`/api/kibana/suggestions/values/${index}`, { method: 'POST', - body: JSON.stringify({ query, field: field.name, filters }), + body: JSON.stringify({ query, field: field.name, fieldMeta: field?.toSpec?.(), filters }), signal, }) .then((r) => { diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts index fa33f00a49879..36569cafd6611 100644 --- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -9,6 +9,7 @@ import sinon from 'sinon'; import { CoreSetup } from 'src/core/public'; +import { SerializedFieldFormat } from 'src/plugins/expressions/public'; import { IFieldType, FieldSpec } from '../../common/index_patterns'; import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../'; import { getFieldFormatsRegistry } from '../test_utils'; @@ -51,6 +52,7 @@ export class StubIndexPattern { _reindexFields: Function; stubSetFieldFormat: Function; fields?: FieldSpec[]; + setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; constructor( pattern: string, @@ -74,6 +76,10 @@ export class StubIndexPattern { this.metaFields = ['_id', '_type', '_source']; this.fieldFormatMap = {}; + this.setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { + this.fieldFormatMap[fieldName] = format; + }; + this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); this.flattenHit = indexPatterns.flattenHitWrapper( (this as unknown) as IndexPattern, diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 9c526f16f9b77..113f7743289a7 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -36,6 +36,7 @@ export function registerValueSuggestionsRoute( field: schema.string(), query: schema.string(), filters: schema.maybe(schema.any()), + fieldMeta: schema.maybe(schema.any()), }, { unknowns: 'allow' } ), @@ -43,7 +44,7 @@ export function registerValueSuggestionsRoute( }, async (context, request, response) => { const config = await config$.pipe(first()).toPromise(); - const { field: fieldName, query, filters } = request.body; + const { field: fieldName, query, filters, fieldMeta } = request.body; const { index } = request.params; const { client } = context.core.elasticsearch.legacy; const signal = getRequestAbortedSignal(request.events.aborted$); @@ -53,9 +54,14 @@ export function registerValueSuggestionsRoute( terminate_after: config.kibana.autocompleteTerminateAfter.asMilliseconds(), }; - const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index); + let field: IFieldType | undefined = fieldMeta; + + if (!field?.name && !field?.type) { + const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index); + + field = indexPattern && getFieldByName(fieldName, indexPattern); + } - const field = indexPattern && getFieldByName(fieldName, indexPattern); const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); const result = await client.callAsCurrentUser('search', { index, body }, { signal }); diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 7db03f726e6f5..6ea22001f5d80 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -12,7 +12,8 @@ "urlForwarding", "navigation", "uiActions", - "savedObjects" + "savedObjects", + "indexPatternFieldEditor" ], "optionalPlugins": ["home", "share", "usageCollection"], "requiredBundles": ["kibanaUtils", "home", "kibanaReact"] diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 45382af098644..35a89eb45f35e 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -458,6 +458,13 @@ function discoverController($route, $scope) { $scope.fetchStatus = fetchStatuses.COMPLETE; } + $scope.refreshAppState = async () => { + $scope.hits = []; + $scope.rows = []; + $scope.fieldCounts = {}; + await refetch$.next(); + }; + function getRequestResponder({ searchSessionId = null } = { searchSessionId: null }) { inspectorAdapters.requests.reset(); const title = i18n.translate('discover.inspectorRequestDataTitle', { diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index f14800f81d08e..fadaffde5c5c3 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -16,6 +16,7 @@ top-nav-menu="topNavMenu" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" + refresh-app-state="refreshAppState" > diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index e7d5ed469525f..9ebeff69d7542 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -177,7 +177,7 @@ export function getState({ }, uiSettings ); - // todo filter source depending on fields fetchinbg flag (if no columns remain and source fetching is enabled, use default columns) + // todo filter source depending on fields fetching flag (if no columns remain and source fetching is enabled, use default columns) let previousAppState: AppState; const appStateContainer = createStateContainer(initialAppState); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts index 050959dff98a4..4c6b9002ce867 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts @@ -90,6 +90,7 @@ describe('Row formatter', () => { }, { 'object.value': [5, 10], + getByName: jest.fn(), }, indexPattern ).trim() @@ -107,7 +108,7 @@ describe('Row formatter', () => { }); const formatted = formatTopLevelObject( { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, - { 'a.zzz': [100], 'a.ccc': [50] }, + { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() }, indexPattern ).trim(); expect(formatted.indexOf('
a.ccc:
')).toBeLessThan(formatted.indexOf('
a.zzz:
')); @@ -134,6 +135,7 @@ describe('Row formatter', () => { { 'object.value': [5, 10], 'object.keys': ['a', 'b'], + getByName: jest.fn(), }, indexPattern ).trim() @@ -154,6 +156,7 @@ describe('Row formatter', () => { }, { 'object.value': [5, 10], + getByName: jest.fn(), }, indexPattern ).trim() diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts index a226cefb53960..02902b0634797 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts @@ -28,11 +28,13 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern) const highlights = hit?.highlight ?? {}; // Keys are sorted in the hits object const formatted = indexPattern.formatHit(hit); + const fields = indexPattern.fields; const highlightPairs: Array<[string, unknown]> = []; const sourcePairs: Array<[string, unknown]> = []; Object.entries(formatted).forEach(([key, val]) => { + const displayKey = fields.getByName ? fields.getByName(key)?.displayName : undefined; const pairs = highlights[key] ? highlightPairs : sourcePairs; - pairs.push([key, val]); + pairs.push([displayKey ? displayKey : key, val]); }); return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] }); }; @@ -48,9 +50,11 @@ export const formatTopLevelObject = ( const sorted = Object.entries(fields).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); sorted.forEach(([key, values]) => { const field = indexPattern.getFieldByName(key); + const displayKey = fields.getByName ? fields.getByName(key)?.displayName : undefined; const formatter = field ? indexPattern.getFormatterForField(field) : { convert: (v: string, ...rest: unknown[]) => String(v) }; + if (!values.map) return; const formatted = values .map((val: unknown) => formatter.convert(val, 'html', { @@ -61,7 +65,7 @@ export const formatTopLevelObject = ( ) .join(', '); const pairs = highlights[key] ? highlightPairs : sourcePairs; - pairs.push([key, formatted]); + pairs.push([displayKey ? displayKey : key, formatted]); }); return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] }); }; diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index 5abf87fdfbc08..cc88ef03c5d03 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -28,5 +28,6 @@ export function createDiscoverDirective(reactDirective: any) { ['updateQuery', { watchDepth: 'reference' }], ['updateSavedQueryId', { watchDepth: 'reference' }], ['unmappedFieldsConfig', { watchDepth: 'value' }], + ['refreshAppState', { watchDepth: 'reference' }], ]); } diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 9615a1c10ea8e..6b71bd892b520 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -68,6 +68,7 @@ export function Discover({ searchSource, state, unmappedFieldsConfig, + refreshAppState, }: DiscoverProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); const scrollableDesktop = useRef(null); @@ -203,6 +204,12 @@ export function Discover({ [opts, state] ); + const onEditRuntimeField = () => { + if (refreshAppState) { + refreshAppState(); + } + }; + const columns = useMemo(() => { if (!state.columns) { return []; @@ -245,6 +252,7 @@ export function Discover({ trackUiMetric={trackUiMetric} unmappedFieldsConfig={unmappedFieldsConfig} useNewFieldsApi={useNewFieldsApi} + onEditRuntimeField={onEditRuntimeField} /> diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index dce0a82934c25..03203a79d9dd0 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -77,6 +77,9 @@ export const getRenderCellValueFn = ( const sourcePairs: Array<[string, string]> = []; Object.entries(innerColumns).forEach(([key, values]) => { const subField = indexPattern.getFieldByName(key); + const displayKey = indexPattern.fields.getByName + ? indexPattern.fields.getByName(key)?.displayName + : undefined; const formatter = subField ? indexPattern.getFormatterForField(subField) : { convert: (v: string, ...rest: unknown[]) => String(v) }; @@ -90,7 +93,7 @@ export const getRenderCellValueFn = ( ) .join(', '); const pairs = highlights[key] ? highlightPairs : sourcePairs; - pairs.push([key, formatted]); + pairs.push([displayKey ? displayKey : key, formatted]); }); return ( @@ -130,7 +133,10 @@ export const getRenderCellValueFn = ( Object.entries(formatted).forEach(([key, val]) => { const pairs = highlights[key] ? highlightPairs : sourcePairs; - pairs.push([key, val as string]); + const displayKey = indexPattern.fields.getByName + ? indexPattern.fields.getByName(key)?.displayName + : undefined; + pairs.push([displayKey ? displayKey : key, val as string]); }); return ( diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap index f3c8990388024..f976b961d8520 100644 --- a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap @@ -543,6 +543,7 @@ exports[`discover sidebar field details footer renders properly 1`] = ` "_source", ], "popularizeField": [Function], + "setFieldFormat": [Function], "stubSetFieldFormat": [Function], "timeFieldName": "time", "title": "logstash-*", diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index b0d71c774f445..a630ddda40f30 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -16,6 +16,8 @@ import { EuiToolTip, EuiTitle, EuiIcon, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -69,6 +71,8 @@ export interface DiscoverFieldProps { trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; multiFields?: Array<{ field: IndexPatternField; isSelected: boolean }>; + + onEditField?: (fieldName: string) => void; } export function DiscoverField({ @@ -82,6 +86,7 @@ export function DiscoverField({ selected, trackUiMetric, multiFields, + onEditField, }: DiscoverFieldProps) { const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { defaultMessage: 'Add {field} to table', @@ -250,7 +255,6 @@ export function DiscoverField({ }; const fieldInfoIcon = getFieldInfoIcon(); - const shouldRenderMultiFields = !!multiFields; const renderMultiFields = () => { if (!multiFields) { @@ -282,6 +286,35 @@ export function DiscoverField({ ); }; + const isRuntimeField = Boolean(indexPattern.getFieldByName(field.name)?.runtimeField); + const isUnknownField = field.type === 'unknown' || field.type === 'unknown_selected'; + const canEditField = onEditField && (!isUnknownField || isRuntimeField); + const displayNameGrow = canEditField ? 9 : 10; + const popoverTitle = ( + + + {field.displayName} + {canEditField && ( + + { + if (onEditField) { + togglePopover(); + onEditField(field.name); + } + }} + iconType="pencil" + data-test-subj={`discoverFieldListPanelEdit-${field.name}`} + aria-label={i18n.translate('discover.fieldChooser.discoverField.editFieldLabel', { + defaultMessage: 'Edit index pattern field', + })} + /> + + )} + + + ); + return ( - - {field.displayName} - + {popoverTitle}
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 947972ce1cfc5..0b3f55b5630cc 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -48,6 +48,12 @@ const mockServices = ({ } }, }, + indexPatternFieldEditor: { + openEditor: jest.fn(), + userPermissions: { + editIndexPattern: jest.fn(), + }, + }, } as unknown) as DiscoverServices; jest.mock('../../../kibana_services', () => ({ @@ -102,6 +108,7 @@ function getCompProps(): DiscoverSidebarProps { fieldFilter: getDefaultFieldFilter(), setFieldFilter: jest.fn(), setAppState: jest.fn(), + onEditRuntimeField: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 1be42e1cd6b17..a3bf2e150d088 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -49,6 +49,17 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { * Change current state of fieldFilter */ setFieldFilter: (next: FieldFilterState) => void; + + /** + * Callback to close the flyout sidebar rendered in a flyout, close flyout + */ + closeFlyout?: () => void; + + /** + * Pass the reference to field editor component to the parent, so it can be properly unmounted + * @param ref reference to the field editor component + */ + setFieldEditorRef?: (ref: () => void | undefined) => void; } export function DiscoverSidebar({ @@ -72,8 +83,14 @@ export function DiscoverSidebar({ useNewFieldsApi = false, useFlyout = false, unmappedFieldsConfig, + onEditRuntimeField, + setFieldEditorRef, + closeFlyout, }: DiscoverSidebarProps) { const [fields, setFields] = useState(null); + const { indexPatternFieldEditor } = services; + const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern(); + const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi; const [scrollContainer, setScrollContainer] = useState(null); const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE); const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE); @@ -220,6 +237,27 @@ export function DiscoverSidebar({ return null; } + const editField = (fieldName: string) => { + if (!canEditIndexPatternField) { + return; + } + const ref = indexPatternFieldEditor.openEditor({ + ctx: { + indexPattern: selectedIndexPattern, + }, + fieldName, + onSave: async () => { + onEditRuntimeField(); + }, + }); + if (setFieldEditorRef) { + setFieldEditorRef(ref); + } + if (closeFlyout) { + closeFlyout(); + } + }; + if (useFlyout) { return (
); @@ -388,6 +427,7 @@ export function DiscoverSidebar({ getDetails={getDetailsByField} trackUiMetric={trackUiMetric} multiFields={multiFields?.get(field.name)} + onEditField={canEditIndexPatternField ? editField : undefined} /> ); @@ -414,6 +454,7 @@ export function DiscoverSidebar({ getDetails={getDetailsByField} trackUiMetric={trackUiMetric} multiFields={multiFields?.get(field.name)} + onEditField={canEditIndexPatternField ? editField : undefined} /> ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 79e8caabd4930..caec61cc501b9 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -102,6 +102,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps { setAppState: jest.fn(), state: {}, trackUiMetric: jest.fn(), + onEditRuntimeField: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index 0808ef47c0dc1..6a16399f0e2e1 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -121,6 +121,8 @@ export interface DiscoverSidebarResponsiveProps { */ showUnmappedFields: boolean; }; + + onEditRuntimeField: () => void; } /** @@ -132,15 +134,42 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFieldEditor = useRef<() => void | undefined>(); + + useEffect(() => { + const cleanup = () => { + if (closeFieldEditor?.current) { + closeFieldEditor?.current(); + } + }; + return () => { + // Make sure to close the editor when unmounting + cleanup(); + }; + }, []); + if (!props.selectedIndexPattern) { return null; } + const setFieldEditorRef = (ref: () => void | undefined) => { + closeFieldEditor.current = ref; + }; + + const closeFlyout = () => { + setIsFlyoutVisible(false); + }; + return ( <> {props.isClosed ? null : ( - + )} @@ -215,6 +244,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) fieldFilter={fieldFilter} setFieldFilter={setFieldFilter} alwaysShowActionButtons={true} + setFieldEditorRef={setFieldEditorRef} + closeFlyout={closeFlyout} /> diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index 23a3cc9a9bc74..93620bc1d6bca 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -167,4 +167,6 @@ export interface DiscoverProps { */ showUnmappedFields: boolean; }; + + refreshAppState?: () => void; } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 252265692d203..cf95d5a85b9f2 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -34,6 +34,7 @@ import { getHistory } from './kibana_services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; +import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; export interface DiscoverServices { addBasePath: (path: string) => string; @@ -59,6 +60,7 @@ export interface DiscoverServices { getEmbeddableInjector: any; uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; + indexPatternFieldEditor: IndexPatternFieldEditorStart; } export async function buildServices( @@ -100,5 +102,6 @@ export async function buildServices( toastNotifications: core.notifications.toasts, uiSettings: core.uiSettings, trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'), + indexPatternFieldEditor: plugins.indexPatternFieldEditor, }; } diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 0e0836e3d9573..692704c92356e 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -62,6 +62,7 @@ import { import { SearchEmbeddableFactory } from './application/embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { replaceUrlHashQuery } from '../../kibana_utils/public/'; +import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -133,6 +134,7 @@ export interface DiscoverStartPlugins { inspector: InspectorPublicPluginStart; savedObjects: SavedObjectsStart; usageCollection?: UsageCollectionSetup; + indexPatternFieldEditor: IndexPatternFieldEditorStart; } const innerAngularName = 'app/discover'; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index ec98199c3423e..c0179ad3c8d20 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -23,6 +23,7 @@ { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, - { "path": "../kibana_legacy/tsconfig.json" } + { "path": "../kibana_legacy/tsconfig.json" }, + { "path": "../index_pattern_field_editor/tsconfig.json"} ] } diff --git a/src/plugins/embeddable/server/index.ts b/src/plugins/embeddable/server/index.ts index 33eaaca9dd69b..aac081f9467b6 100644 --- a/src/plugins/embeddable/server/index.ts +++ b/src/plugins/embeddable/server/index.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { EmbeddableServerPlugin, EmbeddableSetup } from './plugin'; +import { EmbeddableServerPlugin, EmbeddableSetup, EmbeddableStart } from './plugin'; -export { EmbeddableSetup }; +export { EmbeddableSetup, EmbeddableStart }; export { EnhancementRegistryDefinition, EmbeddableRegistryDefinition } from './types'; diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index d3921ab11457c..5c7efec57e93b 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -29,6 +29,11 @@ export interface EmbeddableSetup extends PersistableStateService void; } +// Warning: (ae-missing-release-tag) "EmbeddableStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EmbeddableStart = PersistableStateService; + // Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EnhancementRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 5701a1e375204..68e78199798b4 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -14,7 +14,7 @@ export async function getIndexPatterns( indexPatternManagementStart: IndexPatternManagementStart, indexPatternsService: IndexPatternsContract ) { - const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(); + const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(true); const indexPatternsListItems = await Promise.all( existingIndexPatterns.map(async ({ id, title }) => { const isDefault = defaultIndex === id; diff --git a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap index 2180d6a0fcc4e..939e90d2f2583 100644 --- a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap +++ b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap @@ -12,8 +12,10 @@ exports[`kibana_usage_collection Runs the setup method without issues 5`] = `fal exports[`kibana_usage_collection Runs the setup method without issues 6`] = `false`; -exports[`kibana_usage_collection Runs the setup method without issues 7`] = `true`; +exports[`kibana_usage_collection Runs the setup method without issues 7`] = `false`; -exports[`kibana_usage_collection Runs the setup method without issues 8`] = `false`; +exports[`kibana_usage_collection Runs the setup method without issues 8`] = `true`; -exports[`kibana_usage_collection Runs the setup method without issues 9`] = `true`; +exports[`kibana_usage_collection Runs the setup method without issues 9`] = `false`; + +exports[`kibana_usage_collection Runs the setup method without issues 10`] = `true`; diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.test.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.test.mocks.ts new file mode 100644 index 0000000000000..4a8f269fe5098 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.test.mocks.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { cloudDetectorMock } from './detector/cloud_detector.mock'; + +const mock = cloudDetectorMock.create(); + +export const cloudDetailsMock = mock.getCloudDetails; +export const detectCloudServiceMock = mock.detectCloudService; + +jest.doMock('./detector', () => ({ + CloudDetector: jest.fn().mockImplementation(() => mock), +})); diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.test.ts new file mode 100644 index 0000000000000..1f7617a0e69ce --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { cloudDetailsMock, detectCloudServiceMock } from './cloud_provider_collector.test.mocks'; +import { loggingSystemMock } from '../../../../../core/server/mocks'; +import { + Collector, + createUsageCollectionSetupMock, + createCollectorFetchContextMock, +} from '../../../../usage_collection/server/usage_collection.mock'; + +import { registerCloudProviderUsageCollector } from './cloud_provider_collector'; + +describe('registerCloudProviderUsageCollector', () => { + let collector: Collector; + const logger = loggingSystemMock.createLogger(); + + const usageCollectionMock = createUsageCollectionSetupMock(); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = new Collector(logger, config); + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + + const mockedFetchContext = createCollectorFetchContextMock(); + + beforeEach(() => { + cloudDetailsMock.mockClear(); + detectCloudServiceMock.mockClear(); + registerCloudProviderUsageCollector(usageCollectionMock); + }); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + }); + + test('isReady() => false when cloud details are not available', () => { + cloudDetailsMock.mockReturnValueOnce(undefined); + expect(collector.isReady()).toBe(false); + }); + + test('isReady() => true when cloud details are available', () => { + cloudDetailsMock.mockReturnValueOnce({ foo: true }); + expect(collector.isReady()).toBe(true); + }); + + test('initiates CloudDetector.detectCloudDetails when called', () => { + expect(detectCloudServiceMock).toHaveBeenCalledTimes(1); + }); + + describe('fetch()', () => { + test('returns undefined when no details are available', async () => { + cloudDetailsMock.mockReturnValueOnce(undefined); + await expect(collector.fetch(mockedFetchContext)).resolves.toBeUndefined(); + }); + + test('returns cloud details when defined', async () => { + const mockDetails = { + name: 'aws', + vm_type: 't2.micro', + region: 'us-west-2', + zone: 'us-west-2a', + }; + + cloudDetailsMock.mockReturnValueOnce(mockDetails); + await expect(collector.fetch(mockedFetchContext)).resolves.toEqual(mockDetails); + }); + + test('should not fail if invoked when not ready', async () => { + cloudDetailsMock.mockReturnValueOnce(undefined); + await expect(collector.fetch(mockedFetchContext)).resolves.toBe(undefined); + }); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.ts new file mode 100644 index 0000000000000..eafce56d7cf2e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/cloud_provider_collector.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CloudDetector } from './detector'; + +interface Usage { + name: string; + vm_type?: string; + region?: string; + zone?: string; +} + +export function registerCloudProviderUsageCollector(usageCollection: UsageCollectionSetup) { + const cloudDetector = new CloudDetector(); + // determine the cloud service in the background + cloudDetector.detectCloudService(); + + const collector = usageCollection.makeUsageCollector({ + type: 'cloud_provider', + isReady: () => Boolean(cloudDetector.getCloudDetails()), + async fetch() { + const details = cloudDetector.getCloudDetails(); + if (!details) { + return; + } + + return { + name: details.name, + vm_type: details.vm_type, + region: details.region, + zone: details.zone, + }; + }, + schema: { + name: { + type: 'keyword', + _meta: { + description: 'The name of the cloud provider', + }, + }, + vm_type: { + type: 'keyword', + _meta: { + description: 'The VM instance type', + }, + }, + region: { + type: 'keyword', + _meta: { + description: 'The cloud provider region', + }, + }, + zone: { + type: 'keyword', + _meta: { + description: 'The availability zone within the region', + }, + }, + }, + }); + + usageCollection.registerCollector(collector); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/aws.test.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/aws.test.ts new file mode 100644 index 0000000000000..0bba64823a3e2 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/aws.test.ts @@ -0,0 +1,311 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs'; +import type { Request, RequestOptions } from './cloud_service'; +import { AWSCloudService, AWSResponse } from './aws'; + +type Callback = (err: unknown, res: unknown) => void; + +const AWS = new AWSCloudService(); + +describe('AWS', () => { + const expectedFilenames = ['/sys/hypervisor/uuid', '/sys/devices/virtual/dmi/id/product_uuid']; + const expectedEncoding = 'utf8'; + // mixed case to ensure we check for ec2 after lowercasing + const ec2Uuid = 'eC2abcdef-ghijk\n'; + const ec2FileSystem = { + readFile: (filename: string, encoding: string, callback: Callback) => { + expect(expectedFilenames).toContain(filename); + expect(encoding).toEqual(expectedEncoding); + + callback(null, ec2Uuid); + }, + } as typeof fs; + + it('is named "aws"', () => { + expect(AWS.getName()).toEqual('aws'); + }); + + describe('_checkIfService', () => { + it('handles expected response', async () => { + const id = 'abcdef'; + const request = ((req: RequestOptions, callback: Callback) => { + expect(req.method).toEqual('GET'); + expect(req.uri).toEqual( + 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document' + ); + expect(req.json).toEqual(true); + + const body = `{"instanceId": "${id}","availabilityZone":"us-fake-2c", "imageId" : "ami-6df1e514"}`; + + callback(null, { statusCode: 200, body }); + }) as Request; + // ensure it does not use the fs to trump the body + const awsCheckedFileSystem = new AWSCloudService({ + _fs: ec2FileSystem, + _isWindows: false, + }); + + const response = await awsCheckedFileSystem._checkIfService(request); + + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ + name: AWS.getName(), + id, + region: undefined, + vm_type: undefined, + zone: 'us-fake-2c', + metadata: { + imageId: 'ami-6df1e514', + }, + }); + }); + + it('handles request without a usable body by downgrading to UUID detection', async () => { + const request = ((_req: RequestOptions, callback: Callback) => + callback(null, { statusCode: 404 })) as Request; + const awsCheckedFileSystem = new AWSCloudService({ + _fs: ec2FileSystem, + _isWindows: false, + }); + + const response = await awsCheckedFileSystem._checkIfService(request); + + expect(response.isConfirmed()).toBe(true); + expect(response.toJSON()).toEqual({ + name: AWS.getName(), + id: ec2Uuid.trim().toLowerCase(), + region: undefined, + vm_type: undefined, + zone: undefined, + metadata: undefined, + }); + }); + + it('handles request failure by downgrading to UUID detection', async () => { + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(new Error('expected: request failed'), null)) as Request; + const awsCheckedFileSystem = new AWSCloudService({ + _fs: ec2FileSystem, + _isWindows: false, + }); + + const response = await awsCheckedFileSystem._checkIfService(failedRequest); + + expect(response.isConfirmed()).toBe(true); + expect(response.toJSON()).toEqual({ + name: AWS.getName(), + id: ec2Uuid.trim().toLowerCase(), + region: undefined, + vm_type: undefined, + zone: undefined, + metadata: undefined, + }); + }); + + it('handles not running on AWS', async () => { + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(null, null)) as Request; + const awsIgnoredFileSystem = new AWSCloudService({ + _fs: ec2FileSystem, + _isWindows: true, + }); + + const response = await awsIgnoredFileSystem._checkIfService(failedRequest); + + expect(response.getName()).toEqual(AWS.getName()); + expect(response.isConfirmed()).toBe(false); + }); + }); + + describe('parseBody', () => { + it('parses object in expected format', () => { + const body: AWSResponse = { + devpayProductCodes: null, + privateIp: '10.0.0.38', + availabilityZone: 'us-west-2c', + version: '2010-08-31', + instanceId: 'i-0c7a5b7590a4d811c', + billingProducts: null, + instanceType: 't2.micro', + accountId: '1234567890', + architecture: 'x86_64', + kernelId: null, + ramdiskId: null, + imageId: 'ami-6df1e514', + pendingTime: '2017-07-06T02:09:12Z', + region: 'us-west-2', + marketplaceProductCodes: null, + }; + + const response = AWSCloudService.parseBody(AWS.getName(), body)!; + expect(response).not.toBeNull(); + + expect(response.getName()).toEqual(AWS.getName()); + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ + name: 'aws', + id: 'i-0c7a5b7590a4d811c', + vm_type: 't2.micro', + region: 'us-west-2', + zone: 'us-west-2c', + metadata: { + version: '2010-08-31', + architecture: 'x86_64', + kernelId: null, + marketplaceProductCodes: null, + ramdiskId: null, + imageId: 'ami-6df1e514', + pendingTime: '2017-07-06T02:09:12Z', + }, + }); + }); + + it('ignores unexpected response body', () => { + // @ts-expect-error + expect(AWSCloudService.parseBody(AWS.getName(), undefined)).toBe(null); + // @ts-expect-error + expect(AWSCloudService.parseBody(AWS.getName(), null)).toBe(null); + // @ts-expect-error + expect(AWSCloudService.parseBody(AWS.getName(), {})).toBe(null); + // @ts-expect-error + expect(AWSCloudService.parseBody(AWS.getName(), { privateIp: 'a.b.c.d' })).toBe(null); + }); + }); + + describe('_tryToDetectUuid', () => { + describe('checks the file system for UUID if not Windows', () => { + it('checks /sys/hypervisor/uuid', async () => { + const awsCheckedFileSystem = new AWSCloudService({ + _fs: { + readFile: (filename: string, encoding: string, callback: Callback) => { + expect(expectedFilenames).toContain(filename); + expect(encoding).toEqual(expectedEncoding); + + callback(null, ec2Uuid); + }, + } as typeof fs, + _isWindows: false, + }); + + const response = await awsCheckedFileSystem._tryToDetectUuid(); + + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ + name: AWS.getName(), + id: ec2Uuid.trim().toLowerCase(), + region: undefined, + zone: undefined, + vm_type: undefined, + metadata: undefined, + }); + }); + + it('checks /sys/devices/virtual/dmi/id/product_uuid', async () => { + const awsCheckedFileSystem = new AWSCloudService({ + _fs: { + readFile: (filename: string, encoding: string, callback: Callback) => { + expect(expectedFilenames).toContain(filename); + expect(encoding).toEqual(expectedEncoding); + + callback(null, ec2Uuid); + }, + } as typeof fs, + _isWindows: false, + }); + + const response = await awsCheckedFileSystem._tryToDetectUuid(); + + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ + name: AWS.getName(), + id: ec2Uuid.trim().toLowerCase(), + region: undefined, + zone: undefined, + vm_type: undefined, + metadata: undefined, + }); + }); + + it('returns confirmed if only one file exists', async () => { + let callCount = 0; + const awsCheckedFileSystem = new AWSCloudService({ + _fs: { + readFile: (filename: string, encoding: string, callback: Callback) => { + if (callCount === 0) { + callCount++; + throw new Error('oops'); + } + callback(null, ec2Uuid); + }, + } as typeof fs, + _isWindows: false, + }); + + const response = await awsCheckedFileSystem._tryToDetectUuid(); + + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ + name: AWS.getName(), + id: ec2Uuid.trim().toLowerCase(), + region: undefined, + zone: undefined, + vm_type: undefined, + metadata: undefined, + }); + }); + + it('returns unconfirmed if all files return errors', async () => { + const awsFailedFileSystem = new AWSCloudService({ + _fs: ({ + readFile: () => { + throw new Error('oops'); + }, + } as unknown) as typeof fs, + _isWindows: false, + }); + + const response = await awsFailedFileSystem._tryToDetectUuid(); + + expect(response.isConfirmed()).toEqual(false); + }); + }); + + it('ignores UUID if it does not start with ec2', async () => { + const notEC2FileSystem = { + readFile: (filename: string, encoding: string, callback: Callback) => { + expect(expectedFilenames).toContain(filename); + expect(encoding).toEqual(expectedEncoding); + + callback(null, 'notEC2'); + }, + } as typeof fs; + + const awsCheckedFileSystem = new AWSCloudService({ + _fs: notEC2FileSystem, + _isWindows: false, + }); + + const response = await awsCheckedFileSystem._tryToDetectUuid(); + + expect(response.isConfirmed()).toEqual(false); + }); + + it('does NOT check the file system for UUID on Windows', async () => { + const awsUncheckedFileSystem = new AWSCloudService({ + _fs: ec2FileSystem, + _isWindows: true, + }); + + const response = await awsUncheckedFileSystem._tryToDetectUuid(); + + expect(response.isConfirmed()).toEqual(false); + }); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/aws.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/aws.ts new file mode 100644 index 0000000000000..69e5698489b30 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/aws.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs'; +import { get, isString, omit } from 'lodash'; +import { promisify } from 'util'; +import { CloudService, CloudServiceOptions, Request, RequestOptions } from './cloud_service'; +import { CloudServiceResponse } from './cloud_response'; + +// We explicitly call out the version, 2016-09-02, rather than 'latest' to avoid unexpected changes +const SERVICE_ENDPOINT = 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document'; + +/** @internal */ +export interface AWSResponse { + accountId: string; + architecture: string; + availabilityZone: string; + billingProducts: unknown; + devpayProductCodes: unknown; + marketplaceProductCodes: unknown; + imageId: string; + instanceId: string; + instanceType: string; + kernelId: unknown; + pendingTime: string; + privateIp: string; + ramdiskId: unknown; + region: string; + version: string; +} + +/** + * Checks and loads the service metadata for an Amazon Web Service VM if it is available. + * + * @internal + */ +export class AWSCloudService extends CloudService { + private readonly _isWindows: boolean; + private readonly _fs: typeof fs; + + /** + * Parse the AWS response, if possible. + * + * Example payload: + * { + * "accountId" : "1234567890", + * "architecture" : "x86_64", + * "availabilityZone" : "us-west-2c", + * "billingProducts" : null, + * "devpayProductCodes" : null, + * "imageId" : "ami-6df1e514", + * "instanceId" : "i-0c7a5b7590a4d811c", + * "instanceType" : "t2.micro", + * "kernelId" : null, + * "pendingTime" : "2017-07-06T02:09:12Z", + * "privateIp" : "10.0.0.38", + * "ramdiskId" : null, + * "region" : "us-west-2" + * "version" : "2010-08-31", + * } + */ + static parseBody(name: string, body: AWSResponse): CloudServiceResponse | null { + const id: string | undefined = get(body, 'instanceId'); + const vmType: string | undefined = get(body, 'instanceType'); + const region: string | undefined = get(body, 'region'); + const zone: string | undefined = get(body, 'availabilityZone'); + const metadata = omit(body, [ + // remove keys we already have + 'instanceId', + 'instanceType', + 'region', + 'availabilityZone', + // remove keys that give too much detail + 'accountId', + 'billingProducts', + 'devpayProductCodes', + 'privateIp', + ]); + + // ensure we actually have some data + if (id || vmType || region || zone) { + return new CloudServiceResponse(name, true, { id, vmType, region, zone, metadata }); + } + + return null; + } + + constructor(options: CloudServiceOptions = {}) { + super('aws', options); + + // Allow the file system handler to be swapped out for tests + const { _fs = fs, _isWindows = process.platform.startsWith('win') } = options; + + this._fs = _fs; + this._isWindows = _isWindows; + } + + async _checkIfService(request: Request) { + const req: RequestOptions = { + method: 'GET', + uri: SERVICE_ENDPOINT, + json: true, + }; + + return promisify(request)(req) + .then((response) => + this._parseResponse(response.body, (body) => + AWSCloudService.parseBody(this.getName(), body) + ) + ) + .catch(() => this._tryToDetectUuid()); + } + + /** + * Attempt to load the UUID by checking `/sys/hypervisor/uuid`. + * + * This is a fallback option if the metadata service is unavailable for some reason. + */ + _tryToDetectUuid() { + // Windows does not have an easy way to check + if (!this._isWindows) { + const pathsToCheck = ['/sys/hypervisor/uuid', '/sys/devices/virtual/dmi/id/product_uuid']; + const promises = pathsToCheck.map((path) => promisify(this._fs.readFile)(path, 'utf8')); + + return Promise.allSettled(promises).then((responses) => { + for (const response of responses) { + let uuid; + if (response.status === 'fulfilled' && isString(response.value)) { + // Some AWS APIs return it lowercase (like the file did in testing), while others return it uppercase + uuid = response.value.trim().toLowerCase(); + + // There is a small chance of a false positive here in the unlikely event that a uuid which doesn't + // belong to ec2 happens to be generated with `ec2` as the first three characters. + if (uuid.startsWith('ec2')) { + return new CloudServiceResponse(this._name, true, { id: uuid }); + } + } + } + + return this._createUnconfirmedResponse(); + }); + } + + return Promise.resolve(this._createUnconfirmedResponse()); + } +} diff --git a/x-pack/plugins/monitoring/server/cloud/azure.test.js b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/azure.test.ts similarity index 71% rename from x-pack/plugins/monitoring/server/cloud/azure.test.js rename to src/plugins/kibana_usage_collection/server/collectors/cloud/detector/azure.test.ts index cb56c89f1d64a..17205562fa335 100644 --- a/x-pack/plugins/monitoring/server/cloud/azure.test.js +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/azure.test.ts @@ -1,11 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { AZURE } from './azure'; +import type { Request, RequestOptions } from './cloud_service'; +import { AzureCloudService } from './azure'; + +type Callback = (err: unknown, res: unknown) => void; + +const AZURE = new AzureCloudService(); describe('Azure', () => { it('is named "azure"', () => { @@ -15,16 +21,16 @@ describe('Azure', () => { describe('_checkIfService', () => { it('handles expected response', async () => { const id = 'abcdef'; - const request = (req, callback) => { + const request = ((req: RequestOptions, callback: Callback) => { expect(req.method).toEqual('GET'); expect(req.uri).toEqual('http://169.254.169.254/metadata/instance?api-version=2017-04-02'); - expect(req.headers.Metadata).toEqual('true'); + expect(req.headers?.Metadata).toEqual('true'); expect(req.json).toEqual(true); const body = `{"compute":{"vmId": "${id}","location":"fakeus","availabilityZone":"fakeus-2"}}`; - callback(null, { statusCode: 200, body }, body); - }; + callback(null, { statusCode: 200, body }); + }) as Request; const response = await AZURE._checkIfService(request); expect(response.isConfirmed()).toEqual(true); @@ -43,39 +49,30 @@ describe('Azure', () => { // NOTE: the CloudService method, checkIfService, catches the errors that follow it('handles not running on Azure with error by rethrowing it', async () => { const someError = new Error('expected: request failed'); - const failedRequest = (_req, callback) => callback(someError, null); + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(someError, null)) as Request; - try { + expect(async () => { await AZURE._checkIfService(failedRequest); - - expect().fail('Method should throw exception (Promise.reject)'); - } catch (err) { - expect(err.message).toEqual(someError.message); - } + }).rejects.toThrowError(someError.message); }); it('handles not running on Azure with 404 response by throwing error', async () => { - const failedRequest = (_req, callback) => callback(null, { statusCode: 404 }); + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(null, { statusCode: 404 })) as Request; - try { + expect(async () => { await AZURE._checkIfService(failedRequest); - - expect().fail('Method should throw exception (Promise.reject)'); - } catch (ignoredErr) { - // ignored - } + }).rejects.toThrowErrorMatchingInlineSnapshot(`"Azure request failed"`); }); it('handles not running on Azure with unexpected response by throwing error', async () => { - const failedRequest = (_req, callback) => callback(null, null); + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(null, null)) as Request; - try { + expect(async () => { await AZURE._checkIfService(failedRequest); - - expect().fail('Method should throw exception (Promise.reject)'); - } catch (ignoredErr) { - // ignored - } + }).rejects.toThrowErrorMatchingInlineSnapshot(`"Azure request failed"`); }); }); @@ -122,7 +119,8 @@ describe('Azure', () => { }, }; - const response = AZURE._parseBody(body); + const response = AzureCloudService.parseBody(AZURE.getName(), body)!; + expect(response).not.toBeNull(); expect(response.getName()).toEqual(AZURE.getName()); expect(response.isConfirmed()).toEqual(true); @@ -174,7 +172,8 @@ describe('Azure', () => { }, }; - const response = AZURE._parseBody(body); + const response = AzureCloudService.parseBody(AZURE.getName(), body)!; + expect(response).not.toBeNull(); expect(response.getName()).toEqual(AZURE.getName()); expect(response.isConfirmed()).toEqual(true); @@ -191,10 +190,14 @@ describe('Azure', () => { }); it('ignores unexpected response body', () => { - expect(AZURE._parseBody(undefined)).toBe(null); - expect(AZURE._parseBody(null)).toBe(null); - expect(AZURE._parseBody({})).toBe(null); - expect(AZURE._parseBody({ privateIp: 'a.b.c.d' })).toBe(null); + // @ts-expect-error + expect(AzureCloudService.parseBody(AZURE.getName(), undefined)).toBe(null); + // @ts-expect-error + expect(AzureCloudService.parseBody(AZURE.getName(), null)).toBe(null); + // @ts-expect-error + expect(AzureCloudService.parseBody(AZURE.getName(), {})).toBe(null); + // @ts-expect-error + expect(AzureCloudService.parseBody(AZURE.getName(), { privateIp: 'a.b.c.d' })).toBe(null); }); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/azure.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/azure.ts new file mode 100644 index 0000000000000..b846636f0ce6c --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/azure.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { get, omit } from 'lodash'; +import { promisify } from 'util'; +import { CloudService, Request } from './cloud_service'; +import { CloudServiceResponse } from './cloud_response'; + +// 2017-04-02 is the first GA release of this API +const SERVICE_ENDPOINT = 'http://169.254.169.254/metadata/instance?api-version=2017-04-02'; + +interface AzureResponse { + compute?: Record; + network: unknown; +} + +/** + * Checks and loads the service metadata for an Azure VM if it is available. + * + * @internal + */ +export class AzureCloudService extends CloudService { + /** + * Parse the Azure response, if possible. + * + * Azure VMs created using the "classic" method, as opposed to the resource manager, + * do not provide a "compute" field / object. However, both report the "network" field / object. + * + * Example payload (with network object ignored): + * { + * "compute": { + * "location": "eastus", + * "name": "my-ubuntu-vm", + * "offer": "UbuntuServer", + * "osType": "Linux", + * "platformFaultDomain": "0", + * "platformUpdateDomain": "0", + * "publisher": "Canonical", + * "sku": "16.04-LTS", + * "version": "16.04.201706191", + * "vmId": "d4c57456-2b3b-437a-9f1f-7082cfce02d4", + * "vmSize": "Standard_A1" + * }, + * "network": { + * ... + * } + * } + */ + static parseBody(name: string, body: AzureResponse): CloudServiceResponse | null { + const compute: Record | undefined = get(body, 'compute'); + const id = get, string>(compute, 'vmId'); + const vmType = get, string>(compute, 'vmSize'); + const region = get, string>(compute, 'location'); + + // remove keys that we already have; explicitly undefined so we don't send it when empty + const metadata = compute ? omit(compute, ['vmId', 'vmSize', 'location']) : undefined; + + // we don't actually use network, but we check for its existence to see if this is a response from Azure + const network = get(body, 'network'); + + // ensure we actually have some data + if (id || vmType || region) { + return new CloudServiceResponse(name, true, { id, vmType, region, metadata }); + } else if (network) { + // classic-managed VMs in Azure don't provide compute so we highlight the lack of info + return new CloudServiceResponse(name, true, { metadata: { classic: true } }); + } + + return null; + } + + constructor(options = {}) { + super('azure', options); + } + + async _checkIfService(request: Request) { + const req = { + method: 'GET', + uri: SERVICE_ENDPOINT, + headers: { + // Azure requires this header + Metadata: 'true', + }, + json: true, + }; + + const response = await promisify(request)(req); + + // Note: there is no fallback option for Azure + if (!response || response.statusCode === 404) { + throw new Error('Azure request failed'); + } + + return this._parseResponse(response.body, (body) => + AzureCloudService.parseBody(this.getName(), body) + ); + } +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.mock.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.mock.ts new file mode 100644 index 0000000000000..82e321c93783d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const create = () => { + const mock = { + detectCloudService: jest.fn(), + getCloudDetails: jest.fn(), + }; + + return mock; +}; + +export const cloudDetectorMock = { create }; diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_detector.test.js b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.test.ts similarity index 56% rename from x-pack/plugins/monitoring/server/cloud/cloud_detector.test.js rename to src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.test.ts index 3c4d0dfa724c8..4b88ed5b4064f 100644 --- a/x-pack/plugins/monitoring/server/cloud/cloud_detector.test.js +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.test.ts @@ -1,11 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { CloudDetector } from './cloud_detector'; +import type { CloudService } from './cloud_service'; describe('CloudDetector', () => { const cloudService1 = { @@ -28,8 +30,10 @@ describe('CloudDetector', () => { }; }, }; - // this service is theoretically a better match for the current server, but order dictates that it should - // never be checked (at least until we have some sort of "confidence" metric returned, if we ever run into this problem) + // this service is theoretically a better match for the current server, + // but order dictates that it should never be checked (at least until + // we have some sort of "confidence" metric returned, if we ever run + // into this problem) const cloudService4 = { checkIfService: () => { return { @@ -40,7 +44,12 @@ describe('CloudDetector', () => { }; }, }; - const cloudServices = [cloudService1, cloudService2, cloudService3, cloudService4]; + const cloudServices = ([ + cloudService1, + cloudService2, + cloudService3, + cloudService4, + ] as unknown) as CloudService[]; describe('getCloudDetails', () => { it('returns undefined by default', () => { @@ -51,35 +60,34 @@ describe('CloudDetector', () => { }); describe('detectCloudService', () => { - it('awaits _getCloudService', async () => { + it('returns first match', async () => { const detector = new CloudDetector({ cloudServices }); - expect(detector.getCloudDetails()).toBe(undefined); + expect(detector.getCloudDetails()).toBeUndefined(); await detector.detectCloudService(); - expect(detector.getCloudDetails()).toEqual({ name: 'good-match' }); - }); - }); - - describe('_getCloudService', () => { - it('returns first match', async () => { - const detector = new CloudDetector(); - // note: should never use better-match - expect(await detector._getCloudService(cloudServices)).toEqual({ name: 'good-match' }); + expect(detector.getCloudDetails()).toEqual({ name: 'good-match' }); }); it('returns undefined if none match', async () => { - const detector = new CloudDetector(); + const services = ([cloudService1, cloudService2] as unknown) as CloudService[]; - expect(await detector._getCloudService([cloudService1, cloudService2])).toBe(undefined); - expect(await detector._getCloudService([])).toBe(undefined); + const detector1 = new CloudDetector({ cloudServices: services }); + await detector1.detectCloudService(); + expect(detector1.getCloudDetails()).toBeUndefined(); + + const detector2 = new CloudDetector({ cloudServices: [] }); + await detector2.detectCloudService(); + expect(detector2.getCloudDetails()).toBeUndefined(); }); // this is already tested above, but this just tests it explicitly it('ignores exceptions from cloud services', async () => { - const detector = new CloudDetector(); + const services = ([cloudService2] as unknown) as CloudService[]; + const detector = new CloudDetector({ cloudServices: services }); - expect(await detector._getCloudService([cloudService2])).toBe(undefined); + await detector.detectCloudService(); + expect(detector.getCloudDetails()).toBeUndefined(); }); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.ts new file mode 100644 index 0000000000000..6f6405d9852b6 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_detector.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { CloudService } from './cloud_service'; +import type { CloudServiceResponseJson } from './cloud_response'; + +import { AWSCloudService } from './aws'; +import { AzureCloudService } from './azure'; +import { GCPCloudService } from './gcp'; + +const SUPPORTED_SERVICES = [AWSCloudService, AzureCloudService, GCPCloudService]; + +interface CloudDetectorOptions { + cloudServices?: CloudService[]; +} + +/** + * The `CloudDetector` can be used to asynchronously detect the + * cloud service that Kibana is running within. + * + * @internal + */ +export class CloudDetector { + private readonly cloudServices: CloudService[]; + private cloudDetails?: CloudServiceResponseJson; + + constructor(options: CloudDetectorOptions = {}) { + this.cloudServices = + options.cloudServices ?? SUPPORTED_SERVICES.map((Service) => new Service()); + } + + /** + * Get any cloud details that we have detected. + */ + getCloudDetails() { + return this.cloudDetails; + } + + /** + * Asynchronously detect the cloud service. + * + * Callers are _not_ expected to await this method, which allows the + * caller to trigger the lookup and then simply use it whenever we + * determine it. + */ + async detectCloudService() { + this.cloudDetails = await this.getCloudService(); + } + + /** + * Check every cloud service until the first one reports success from detection. + */ + private async getCloudService() { + // check each service until we find one that is confirmed to match; + // order is assumed to matter + for (const service of this.cloudServices) { + try { + const serviceResponse = await service.checkIfService(); + + if (serviceResponse.isConfirmed()) { + return serviceResponse.toJSON(); + } + } catch (ignoredError) { + // ignored until we make wider use of this in the UI + } + } + + // explicitly undefined rather than null so that it can be ignored in JSON + return undefined; + } +} diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_response.test.js b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_response.test.ts similarity index 87% rename from x-pack/plugins/monitoring/server/cloud/cloud_response.test.js rename to src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_response.test.ts index fbc0d857ebd02..5fc721929ee85 100644 --- a/x-pack/plugins/monitoring/server/cloud/cloud_response.test.js +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_response.test.ts @@ -1,8 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { CloudServiceResponse } from './cloud_response'; diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_response.js b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_response.ts similarity index 52% rename from x-pack/plugins/monitoring/server/cloud/cloud_response.js rename to src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_response.ts index 5744744dd214e..48291ebff22e7 100644 --- a/x-pack/plugins/monitoring/server/cloud/cloud_response.js +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_response.ts @@ -1,36 +1,63 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ +interface CloudServiceResponseOptions { + id?: string; + vmType?: string; + region?: string; + zone?: string; + metadata?: Record; +} + +export interface CloudServiceResponseJson { + name: string; + id?: string; + vm_type?: string; + region?: string; + zone?: string; + metadata?: Record; +} + /** - * {@code CloudServiceResponse} represents a single response from any individual {@code CloudService}. + * Represents a single response from any individual CloudService. */ export class CloudServiceResponse { + private readonly _name: string; + private readonly _confirmed: boolean; + private readonly _id?: string; + private readonly _vmType?: string; + private readonly _region?: string; + private readonly _zone?: string; + private readonly _metadata?: Record; + /** - * Create an unconfirmed {@code CloudServiceResponse} by the {@code name}. - * - * @param {String} name The name of the {@code CloudService}. - * @return {CloudServiceResponse} Never {@code null}. + * Create an unconfirmed CloudServiceResponse by the name. */ - static unconfirmed(name) { + static unconfirmed(name: string) { return new CloudServiceResponse(name, false, {}); } /** - * Create a new {@code CloudServiceResponse}. + * Create a new CloudServiceResponse. * - * @param {String} name The name of the {@code CloudService}. - * @param {Boolean} confirmed Confirmed to be the current {@code CloudService}. + * @param {String} name The name of the CloudService. + * @param {Boolean} confirmed Confirmed to be the current CloudService. * @param {String} id The optional ID of the VM (depends on the cloud service). * @param {String} vmType The optional type of VM (depends on the cloud service). * @param {String} region The optional region of the VM (depends on the cloud service). * @param {String} availabilityZone The optional availability zone within the region (depends on the cloud service). * @param {Object} metadata The optional metadata associated with the VM. */ - constructor(name, confirmed, { id, vmType, region, zone, metadata }) { + constructor( + name: string, + confirmed: boolean, + { id, vmType, region, zone, metadata }: CloudServiceResponseOptions + ) { this._name = name; this._confirmed = confirmed; this._id = id; @@ -41,9 +68,7 @@ export class CloudServiceResponse { } /** - * Get the name of the {@code CloudService} associated with the current response. - * - * @return {String} The cloud service that created this response. + * Get the name of the CloudService associated with the current response. */ getName() { return this._name; @@ -51,8 +76,6 @@ export class CloudServiceResponse { /** * Determine if the Cloud Service is confirmed to exist. - * - * @return {Boolean} {@code true} to indicate that Kibana is running in this cloud environment. */ isConfirmed() { return this._confirmed; @@ -60,11 +83,8 @@ export class CloudServiceResponse { /** * Create a plain JSON object that can be indexed that represents the response. - * - * @return {Object} Never {@code null} object. - * @throws {Error} if this response is not {@code confirmed}. */ - toJSON() { + toJSON(): CloudServiceResponseJson { if (!this._confirmed) { throw new Error(`[${this._name}] is not confirmed`); } diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_service.test.js b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_service.test.ts similarity index 65% rename from x-pack/plugins/monitoring/server/cloud/cloud_service.test.js rename to src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_service.test.ts index 5a0186d9f9b59..0a7d5899486ab 100644 --- a/x-pack/plugins/monitoring/server/cloud/cloud_service.test.js +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_service.test.ts @@ -1,14 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { CloudService } from './cloud_service'; +import { CloudService, Response } from './cloud_service'; import { CloudServiceResponse } from './cloud_response'; describe('CloudService', () => { + // @ts-expect-error Creating an instance of an abstract class for testing const service = new CloudService('xyz'); describe('getName', () => { @@ -28,13 +30,9 @@ describe('CloudService', () => { describe('_checkIfService', () => { it('throws an exception unless overridden', async () => { - const request = jest.fn(); - - try { - await service._checkIfService(request); - } catch (err) { - expect(err.message).toEqual('not implemented'); - } + expect(async () => { + await service._checkIfService(undefined); + }).rejects.toThrowErrorMatchingInlineSnapshot(`"not implemented"`); }); }); @@ -89,42 +87,46 @@ describe('CloudService', () => { describe('_parseResponse', () => { const body = { some: { body: {} } }; - const tryParseResponse = async (...args) => { - try { - await service._parseResponse(...args); - } catch (err) { - // expected - return; - } - - expect().fail('Should throw exception'); - }; it('throws error upon failure to parse body as object', async () => { - // missing body - await tryParseResponse(); - await tryParseResponse(null); - await tryParseResponse({}); - await tryParseResponse(123); - await tryParseResponse('raw string'); - // malformed JSON object - await tryParseResponse('{{}'); + expect(async () => { + await service._parseResponse(); + }).rejects.toMatchInlineSnapshot(`undefined`); + expect(async () => { + await service._parseResponse(null); + }).rejects.toMatchInlineSnapshot(`undefined`); + expect(async () => { + await service._parseResponse({}); + }).rejects.toMatchInlineSnapshot(`undefined`); + expect(async () => { + await service._parseResponse(123); + }).rejects.toMatchInlineSnapshot(`undefined`); + expect(async () => { + await service._parseResponse('raw string'); + }).rejects.toMatchInlineSnapshot(`[Error: 'raw string' is not a JSON object]`); + expect(async () => { + await service._parseResponse('{{}'); + }).rejects.toMatchInlineSnapshot(`[Error: '{{}' is not a JSON object]`); }); it('expects unusable bodies', async () => { - const parseBody = (parsedBody) => { + const parseBody = (parsedBody: Response['body']) => { expect(parsedBody).toEqual(body); return null; }; - await tryParseResponse(JSON.stringify(body), parseBody); - await tryParseResponse(body, parseBody); + expect(async () => { + await service._parseResponse(JSON.stringify(body), parseBody); + }).rejects.toMatchInlineSnapshot(`undefined`); + expect(async () => { + await service._parseResponse(body, parseBody); + }).rejects.toMatchInlineSnapshot(`undefined`); }); it('uses parsed object to create response', async () => { const serviceResponse = new CloudServiceResponse('a123', true, { id: 'xyz' }); - const parseBody = (parsedBody) => { + const parseBody = (parsedBody: Response['body']) => { expect(parsedBody).toEqual(body); return serviceResponse; diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_service.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_service.ts new file mode 100644 index 0000000000000..768a46a457d7d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/cloud_service.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs'; +import { isObject, isString, isPlainObject } from 'lodash'; +import defaultRequest from 'request'; +import type { OptionsWithUri, Response as DefaultResponse } from 'request'; +import { CloudServiceResponse } from './cloud_response'; + +/** @internal */ +export type Request = typeof defaultRequest; + +/** @internal */ +export type RequestOptions = OptionsWithUri; + +/** @internal */ +export type Response = DefaultResponse; + +/** @internal */ +export interface CloudServiceOptions { + _request?: Request; + _fs?: typeof fs; + _isWindows?: boolean; +} + +/** + * CloudService provides a mechanism for cloud services to be checked for + * metadata that may help to determine the best defaults and priorities. + */ +export abstract class CloudService { + private readonly _request: Request; + protected readonly _name: string; + + constructor(name: string, options: CloudServiceOptions = {}) { + this._name = name.toLowerCase(); + + // Allow the HTTP handler to be swapped out for tests + const { _request = defaultRequest } = options; + + this._request = _request; + } + + /** + * Get the search-friendly name of the Cloud Service. + */ + getName() { + return this._name; + } + + /** + * Using whatever mechanism is required by the current Cloud Service, + * determine if Kibana is running in it and return relevant metadata. + */ + async checkIfService() { + try { + return await this._checkIfService(this._request); + } catch (e) { + return this._createUnconfirmedResponse(); + } + } + + _checkIfService(request: Request): Promise { + // should always be overridden by a subclass + return Promise.reject(new Error('not implemented')); + } + + /** + * Create a new CloudServiceResponse that denotes that this cloud service + * is not being used by the current machine / VM. + */ + _createUnconfirmedResponse() { + return CloudServiceResponse.unconfirmed(this._name); + } + + /** + * Strictly parse JSON. + */ + _stringToJson(value: string) { + // note: this will throw an error if this is not a string + value = value.trim(); + + try { + const json = JSON.parse(value); + // we don't want to return scalar values, arrays, etc. + if (!isPlainObject(json)) { + throw new Error('not a plain object'); + } + return json; + } catch (e) { + throw new Error(`'${value}' is not a JSON object`); + } + } + + /** + * Convert the response to a JSON object and attempt to parse it using the + * parseBody function. + * + * If the response cannot be parsed as a JSON object, or if it fails to be + * useful, then parseBody should return null. + */ + _parseResponse( + body: Response['body'], + parseBody?: (body: Response['body']) => CloudServiceResponse | null + ): Promise { + // parse it if necessary + if (isString(body)) { + try { + body = this._stringToJson(body); + } catch (err) { + return Promise.reject(err); + } + } + + if (isObject(body) && parseBody) { + const response = parseBody(body); + + if (response) { + return Promise.resolve(response); + } + } + + // use default handling + return Promise.reject(); + } +} diff --git a/x-pack/plugins/monitoring/server/cloud/gcp.test.js b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/gcp.test.ts similarity index 66% rename from x-pack/plugins/monitoring/server/cloud/gcp.test.js rename to src/plugins/kibana_usage_collection/server/collectors/cloud/detector/gcp.test.ts index 803c6f31af3b9..fd0b3331b4ad1 100644 --- a/x-pack/plugins/monitoring/server/cloud/gcp.test.js +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/gcp.test.ts @@ -1,11 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { GCP } from './gcp'; +import type { Request, RequestOptions } from './cloud_service'; +import { GCPCloudService } from './gcp'; + +type Callback = (err: unknown, res: unknown) => void; + +const GCP = new GCPCloudService(); describe('GCP', () => { it('is named "gcp"', () => { @@ -17,30 +23,28 @@ describe('GCP', () => { const headers = { 'metadata-flavor': 'Google' }; it('handles expected responses', async () => { - const metadata = { + const metadata: Record = { id: 'abcdef', 'machine-type': 'projects/441331612345/machineTypes/f1-micro', zone: 'projects/441331612345/zones/us-fake4-c', }; - const request = (req, callback) => { + const request = ((req: RequestOptions, callback: Callback) => { const basePath = 'http://169.254.169.254/computeMetadata/v1/instance/'; expect(req.method).toEqual('GET'); - expect(req.uri.startsWith(basePath)).toBe(true); - expect(req.headers['Metadata-Flavor']).toEqual('Google'); + expect((req.uri as string).startsWith(basePath)).toBe(true); + expect(req.headers!['Metadata-Flavor']).toEqual('Google'); expect(req.json).toEqual(false); - const requestKey = req.uri.substring(basePath.length); + const requestKey = (req.uri as string).substring(basePath.length); let body = null; if (metadata[requestKey]) { body = metadata[requestKey]; - } else { - expect().fail(`Unknown field requested [${requestKey}]`); } - callback(null, { statusCode: 200, body, headers }, body); - }; + callback(null, { statusCode: 200, body, headers }); + }) as Request; const response = await GCP._checkIfService(request); expect(response.isConfirmed()).toEqual(true); @@ -56,79 +60,63 @@ describe('GCP', () => { // NOTE: the CloudService method, checkIfService, catches the errors that follow it('handles unexpected responses', async () => { - const request = (_req, callback) => callback(null, { statusCode: 200, headers }); + const request = ((_req: RequestOptions, callback: Callback) => + callback(null, { statusCode: 200, headers })) as Request; - try { + expect(async () => { await GCP._checkIfService(request); - } catch (err) { - // ignored - return; - } - - expect().fail('Method should throw exception (Promise.reject)'); + }).rejects.toThrowErrorMatchingInlineSnapshot(`"unrecognized responses"`); }); it('handles unexpected responses without response header', async () => { const body = 'xyz'; - const request = (_req, callback) => callback(null, { statusCode: 200, body }, body); - - try { - await GCP._checkIfService(request); - } catch (err) { - // ignored - return; - } + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(null, { statusCode: 200, body })) as Request; - expect().fail('Method should throw exception (Promise.reject)'); + expect(async () => { + await GCP._checkIfService(failedRequest); + }).rejects.toThrowErrorMatchingInlineSnapshot(`"unrecognized responses"`); }); it('handles not running on GCP with error by rethrowing it', async () => { const someError = new Error('expected: request failed'); - const failedRequest = (_req, callback) => callback(someError, null); + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(someError, null)) as Request; - try { + expect(async () => { await GCP._checkIfService(failedRequest); - - expect().fail('Method should throw exception (Promise.reject)'); - } catch (err) { - expect(err.message).toEqual(someError.message); - } + }).rejects.toThrowError(someError); }); it('handles not running on GCP with 404 response by throwing error', async () => { const body = 'This is some random error text'; - const failedRequest = (_req, callback) => - callback(null, { statusCode: 404, headers, body }, body); + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(null, { statusCode: 404, headers, body })) as Request; - try { + expect(async () => { await GCP._checkIfService(failedRequest); - } catch (err) { - // ignored - return; - } - - expect().fail('Method should throw exception (Promise.reject)'); + }).rejects.toThrowErrorMatchingInlineSnapshot(`"GCP request failed"`); }); it('handles not running on GCP with unexpected response by throwing error', async () => { - const failedRequest = (_req, callback) => callback(null, null); + const failedRequest = ((_req: RequestOptions, callback: Callback) => + callback(null, null)) as Request; - try { + expect(async () => { await GCP._checkIfService(failedRequest); - } catch (err) { - // ignored - return; - } - - expect().fail('Method should throw exception (Promise.reject)'); + }).rejects.toThrowErrorMatchingInlineSnapshot(`"GCP request failed"`); }); }); describe('_extractValue', () => { it('only handles strings', () => { + // @ts-expect-error expect(GCP._extractValue()).toBe(undefined); + // @ts-expect-error expect(GCP._extractValue(null, null)).toBe(undefined); + // @ts-expect-error expect(GCP._extractValue('abc', { field: 'abcxyz' })).toBe(undefined); + // @ts-expect-error expect(GCP._extractValue('abc', 1234)).toBe(undefined); expect(GCP._extractValue('abc/', 'abc/xyz')).toEqual('xyz'); }); @@ -179,12 +167,17 @@ describe('GCP', () => { }); it('ignores unexpected response body', () => { + // @ts-expect-error expect(() => GCP._combineResponses()).toThrow(); + // @ts-expect-error expect(() => GCP._combineResponses(undefined, undefined, undefined)).toThrow(); + // @ts-expect-error expect(() => GCP._combineResponses(null, null, null)).toThrow(); expect(() => + // @ts-expect-error GCP._combineResponses({ id: 'x' }, { machineType: 'a' }, { zone: 'b' }) ).toThrow(); + // @ts-expect-error expect(() => GCP._combineResponses({ privateIp: 'a.b.c.d' })).toThrow(); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/gcp.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/gcp.ts new file mode 100644 index 0000000000000..565c07abd1d2c --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/gcp.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isString } from 'lodash'; +import { promisify } from 'util'; +import { CloudService, CloudServiceOptions, Request, Response } from './cloud_service'; +import { CloudServiceResponse } from './cloud_response'; + +// GCP documentation shows both 'metadata.google.internal' (mostly) and '169.254.169.254' (sometimes) +// To bypass potential DNS changes, the IP was used because it's shared with other cloud services +const SERVICE_ENDPOINT = 'http://169.254.169.254/computeMetadata/v1/instance'; + +/** + * Checks and loads the service metadata for an Google Cloud Platform VM if it is available. + * + * @internal + */ +export class GCPCloudService extends CloudService { + constructor(options: CloudServiceOptions = {}) { + super('gcp', options); + } + + _checkIfService(request: Request) { + // we need to call GCP individually for each field we want metadata for + const fields = ['id', 'machine-type', 'zone']; + + const create = this._createRequestForField; + const allRequests = fields.map((field) => promisify(request)(create(field))); + return ( + Promise.all(allRequests) + // Note: there is no fallback option for GCP; + // responses are arrays containing [fullResponse, body]; + // because GCP returns plaintext, we have no way of validating + // without using the response code. + .then((responses) => { + return responses.map((response) => { + if (!response || response.statusCode === 404) { + throw new Error('GCP request failed'); + } + return this._extractBody(response, response.body); + }); + }) + .then(([id, machineType, zone]) => this._combineResponses(id, machineType, zone)) + ); + } + + _createRequestForField(field: string) { + return { + method: 'GET', + uri: `${SERVICE_ENDPOINT}/${field}`, + headers: { + // GCP requires this header + 'Metadata-Flavor': 'Google', + }, + // GCP does _not_ return JSON + json: false, + }; + } + + /** + * Extract the body if the response is valid and it came from GCP. + */ + _extractBody(response: Response, body?: Response['body']) { + if ( + response?.statusCode === 200 && + response.headers && + response.headers['metadata-flavor'] === 'Google' + ) { + return body; + } + + return null; + } + + /** + * Parse the GCP responses, if possible. + * + * Example values for each parameter: + * + * vmId: '5702733457649812345' + * machineType: 'projects/441331612345/machineTypes/f1-micro' + * zone: 'projects/441331612345/zones/us-east4-c' + */ + _combineResponses(id: string, machineType: string, zone: string) { + const vmId = isString(id) ? id.trim() : undefined; + const vmType = this._extractValue('machineTypes/', machineType); + const vmZone = this._extractValue('zones/', zone); + + let region; + + if (vmZone) { + // converts 'us-east4-c' into 'us-east4' + region = vmZone.substring(0, vmZone.lastIndexOf('-')); + } + + // ensure we actually have some data + if (vmId || vmType || region || vmZone) { + return new CloudServiceResponse(this._name, true, { id: vmId, vmType, region, zone: vmZone }); + } + + throw new Error('unrecognized responses'); + } + + /** + * Extract the useful information returned from GCP while discarding + * unwanted account details (the project ID). + * + * For example, this turns something like + * 'projects/441331612345/machineTypes/f1-micro' into 'f1-micro'. + */ + _extractValue(fieldPrefix: string, value: string) { + if (isString(value)) { + const index = value.lastIndexOf(fieldPrefix); + + if (index !== -1) { + return value.substring(index + fieldPrefix.length).trim(); + } + } + + return undefined; + } +} diff --git a/x-pack/plugins/monitoring/server/cloud/index.js b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/index.ts similarity index 53% rename from x-pack/plugins/monitoring/server/cloud/index.js rename to src/plugins/kibana_usage_collection/server/collectors/cloud/detector/index.ts index 5b64a0be96216..ce82cadb15ad5 100644 --- a/x-pack/plugins/monitoring/server/cloud/index.js +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/detector/index.ts @@ -1,9 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ export { CloudDetector } from './cloud_detector'; -export { CLOUD_SERVICES } from './cloud_services'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/cloud/index.ts b/src/plugins/kibana_usage_collection/server/collectors/cloud/index.ts new file mode 100644 index 0000000000000..7e2c7c891305f --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/cloud/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerCloudProviderUsageCollector } from './cloud_provider_collector'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 10156b51ac183..89e1e6e79482c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -11,6 +11,7 @@ export { registerManagementUsageCollector } from './management'; export { registerApplicationUsageCollector } from './application_usage'; export { registerKibanaUsageCollector } from './kibana'; export { registerOpsStatsCollector } from './ops_stats'; +export { registerCloudProviderUsageCollector } from './cloud'; export { registerCspCollector } from './csp'; export { registerCoreUsageCollector } from './core'; export { registerLocalizationUsageCollector } from './localization'; diff --git a/src/plugins/kibana_usage_collection/server/index.test.mocks.ts b/src/plugins/kibana_usage_collection/server/index.test.mocks.ts new file mode 100644 index 0000000000000..7df27a3719e92 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/index.test.mocks.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { cloudDetectorMock } from './collectors/cloud/detector/cloud_detector.mock'; + +const mock = cloudDetectorMock.create(); + +export const cloudDetailsMock = mock.getCloudDetails; +export const detectCloudServiceMock = mock.detectCloudService; + +jest.doMock('./collectors/cloud/detector', () => ({ + CloudDetector: jest.fn().mockImplementation(() => mock), +})); diff --git a/src/plugins/kibana_usage_collection/server/index.test.ts b/src/plugins/kibana_usage_collection/server/index.test.ts index ee6df366b788f..b4c52f8353d79 100644 --- a/src/plugins/kibana_usage_collection/server/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/index.test.ts @@ -15,6 +15,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '../../usage_collection/server/usage_collection.mock'; +import { cloudDetailsMock } from './index.test.mocks'; import { plugin } from './'; @@ -33,6 +34,10 @@ describe('kibana_usage_collection', () => { return createUsageCollectionSetupMock().makeStatsCollector(opts); }); + beforeEach(() => { + cloudDetailsMock.mockClear(); + }); + test('Runs the setup method without issues', () => { const coreSetup = coreMock.createSetup(); @@ -50,6 +55,12 @@ describe('kibana_usage_collection', () => { coreStart.uiSettings.asScopedToClient.mockImplementation(() => uiSettingsServiceMock.createClient() ); + cloudDetailsMock.mockReturnValueOnce({ + name: 'my-cloud', + vm_type: 'big', + region: 'my-home', + zone: 'my-home-office', + }); expect(pluginInstance.start(coreStart)).toBe(undefined); usageCollectors.forEach(({ isReady }) => { diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 5b903489e3ff3..74d2d281ff8f6 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -28,6 +28,7 @@ import { registerManagementUsageCollector, registerOpsStatsCollector, registerUiMetricUsageCollector, + registerCloudProviderUsageCollector, registerCspCollector, registerCoreUsageCollector, registerLocalizationUsageCollector, @@ -102,6 +103,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { registerType, getSavedObjectsClient ); + registerCloudProviderUsageCollector(usageCollection); registerCspCollector(usageCollection, coreSetup.http); registerCoreUsageCollector(usageCollection, getCoreUsageDataService); registerLocalizationUsageCollector(usageCollection, coreSetup.i18n); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index ddfb8f421f85d..9a13540f8ba05 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -6445,6 +6445,34 @@ } } }, + "cloud_provider": { + "properties": { + "name": { + "type": "keyword", + "_meta": { + "description": "The name of the cloud provider" + } + }, + "vm_type": { + "type": "keyword", + "_meta": { + "description": "The VM instance type" + } + }, + "region": { + "type": "keyword", + "_meta": { + "description": "The cloud provider region" + } + }, + "zone": { + "type": "keyword", + "_meta": { + "description": "The availability zone within the region" + } + } + } + }, "core": { "properties": { "config": { diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.test.ts b/src/plugins/vis_type_timeseries/common/calculate_label.test.ts index d5277623a136d..eab9665436c01 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.test.ts +++ b/src/plugins/vis_type_timeseries/common/calculate_label.test.ts @@ -8,6 +8,7 @@ import { calculateLabel } from './calculate_label'; import type { MetricsItemsSchema } from './types'; +import { SanitizedFieldType } from './types'; describe('calculateLabel(metric, metrics)', () => { test('returns the metric.alias if set', () => { @@ -82,4 +83,26 @@ describe('calculateLabel(metric, metrics)', () => { expect(label).toEqual('Derivative of Outbound Traffic'); }); + + test('should throw an error if field not found', () => { + const metric = ({ id: 2, type: 'max', field: 3 } as unknown) as MetricsItemsSchema; + const metrics = ([ + { id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' }, + metric, + ] as unknown) as MetricsItemsSchema[]; + const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }]; + + expect(() => calculateLabel(metric, metrics, fields)).toThrowError('Field "3" not found'); + }); + + test('should not throw an error if field not found (isThrowErrorOnFieldNotFound is false)', () => { + const metric = ({ id: 2, type: 'max', field: 3 } as unknown) as MetricsItemsSchema; + const metrics = ([ + { id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' }, + metric, + ] as unknown) as MetricsItemsSchema[]; + const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }]; + + expect(calculateLabel(metric, metrics, fields, false)).toBe('Max of 3'); + }); }); diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.ts b/src/plugins/vis_type_timeseries/common/calculate_label.ts index 73b5d3f652644..bd1482e14f4f4 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.ts +++ b/src/plugins/vis_type_timeseries/common/calculate_label.ts @@ -10,6 +10,7 @@ import { includes, startsWith } from 'lodash'; import { i18n } from '@kbn/i18n'; import { lookup } from './agg_lookup'; import { MetricsItemsSchema, SanitizedFieldType } from './types'; +import { extractFieldLabel } from './fields_utils'; const paths = [ 'cumulative_sum', @@ -26,14 +27,11 @@ const paths = [ 'positive_only', ]; -export const extractFieldLabel = (fields: SanitizedFieldType[], name: string) => { - return fields.find((f) => f.name === name)?.label ?? name; -}; - export const calculateLabel = ( metric: MetricsItemsSchema, metrics: MetricsItemsSchema[] = [], - fields: SanitizedFieldType[] = [] + fields: SanitizedFieldType[] = [], + isThrowErrorOnFieldNotFound: boolean = true ): string => { if (!metric) { return i18n.translate('visTypeTimeseries.calculateLabel.unknownLabel', { @@ -71,7 +69,7 @@ export const calculateLabel = ( if (metric.type === 'positive_rate') { return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', { defaultMessage: 'Counter Rate of {field}', - values: { field: extractFieldLabel(fields, metric.field!) }, + values: { field: extractFieldLabel(fields, metric.field!, isThrowErrorOnFieldNotFound) }, }); } if (metric.type === 'static') { @@ -115,7 +113,7 @@ export const calculateLabel = ( defaultMessage: '{lookupMetricType} of {metricField}', values: { lookupMetricType: lookup[metric.type], - metricField: extractFieldLabel(fields, metric.field!), + metricField: extractFieldLabel(fields, metric.field!, isThrowErrorOnFieldNotFound), }, }); }; diff --git a/src/plugins/vis_type_timeseries/common/constants.ts b/src/plugins/vis_type_timeseries/common/constants.ts index 66617c8518985..1debfaf951e99 100644 --- a/src/plugins/vis_type_timeseries/common/constants.ts +++ b/src/plugins/vis_type_timeseries/common/constants.ts @@ -13,3 +13,4 @@ export const ROUTES = { VIS_DATA: '/api/metrics/vis/data', FIELDS: '/api/metrics/fields', }; +export const USE_KIBANA_INDEXES_KEY = 'use_kibana_indexes'; diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.test.ts b/src/plugins/vis_type_timeseries/common/fields_utils.test.ts index d1036aab2dc3e..9550697e22851 100644 --- a/src/plugins/vis_type_timeseries/common/fields_utils.test.ts +++ b/src/plugins/vis_type_timeseries/common/fields_utils.test.ts @@ -7,7 +7,7 @@ */ import { toSanitizedFieldType } from './fields_utils'; -import type { FieldSpec, RuntimeField } from '../../data/common'; +import type { FieldSpec } from '../../data/common'; describe('fields_utils', () => { describe('toSanitizedFieldType', () => { @@ -34,17 +34,6 @@ describe('fields_utils', () => { `); }); - test('should filter runtime fields', async () => { - const fields: FieldSpec[] = [ - { - ...mockedField, - runtimeField: {} as RuntimeField, - }, - ]; - - expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); - }); - test('should filter non-aggregatable fields', async () => { const fields: FieldSpec[] = [ { diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.ts b/src/plugins/vis_type_timeseries/common/fields_utils.ts index 04499d5320ab8..6a83dd323b3fd 100644 --- a/src/plugins/vis_type_timeseries/common/fields_utils.ts +++ b/src/plugins/vis_type_timeseries/common/fields_utils.ts @@ -6,17 +6,60 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import { FieldSpec } from '../../data/common'; import { isNestedField } from '../../data/common'; -import { SanitizedFieldType } from './types'; +import { FetchedIndexPattern, SanitizedFieldType } from './types'; -export const toSanitizedFieldType = (fields: FieldSpec[]) => { - return fields - .filter( - (field) => - // Make sure to only include mapped fields, e.g. no index pattern runtime fields - !field.runtimeField && field.aggregatable && !isNestedField(field) - ) +export class FieldNotFoundError extends Error { + constructor(name: string) { + super( + i18n.translate('visTypeTimeseries.fields.fieldNotFound', { + defaultMessage: `Field "{field}" not found`, + values: { field: name }, + }) + ); + } + + public get name() { + return this.constructor.name; + } + + public get body() { + return this.message; + } +} + +export const extractFieldLabel = ( + fields: SanitizedFieldType[], + name: string, + isThrowErrorOnFieldNotFound: boolean = true +) => { + if (fields.length && name) { + const field = fields.find((f) => f.name === name); + + if (field) { + return field.label || field.name; + } + if (isThrowErrorOnFieldNotFound) { + throw new FieldNotFoundError(name); + } + } + return name; +}; + +export function validateField(name: string, index: FetchedIndexPattern) { + if (name && index.indexPattern) { + const field = index.indexPattern.fields.find((f) => f.name === name); + if (!field) { + throw new FieldNotFoundError(name); + } + } +} + +export const toSanitizedFieldType = (fields: FieldSpec[]) => + fields + .filter((field) => field.aggregatable && !isNestedField(field)) .map( (field) => ({ @@ -25,4 +68,3 @@ export const toSanitizedFieldType = (fields: FieldSpec[]) => { type: field.type, } as SanitizedFieldType) ); -}; diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts index 0428e6e80ae78..1111a9c525243 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts @@ -81,7 +81,7 @@ describe('fetchIndexPattern', () => { }); describe('text-based index', () => { - test('should return the Kibana index if it exists', async () => { + test('should return the Kibana index if it exists (fetchKibabaIndexForStringIndexes is true)', async () => { mockedIndices = [ { id: 'indexId', @@ -89,7 +89,9 @@ describe('fetchIndexPattern', () => { }, ] as IndexPattern[]; - const value = await fetchIndexPattern('indexTitle', indexPatternsService); + const value = await fetchIndexPattern('indexTitle', indexPatternsService, { + fetchKibabaIndexForStringIndexes: true, + }); expect(value).toMatchInlineSnapshot(` Object { @@ -102,8 +104,10 @@ describe('fetchIndexPattern', () => { `); }); - test('should return only indexPatternString if Kibana index does not exist', async () => { - const value = await fetchIndexPattern('indexTitle', indexPatternsService); + test('should return only indexPatternString if Kibana index does not exist (fetchKibabaIndexForStringIndexes is true)', async () => { + const value = await fetchIndexPattern('indexTitle', indexPatternsService, { + fetchKibabaIndexForStringIndexes: true, + }); expect(value).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts index af9f0750b2604..5dacad338e7a8 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts +++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts @@ -52,7 +52,12 @@ export const extractIndexPatternValues = ( export const fetchIndexPattern = async ( indexPatternValue: IndexPatternValue | undefined, - indexPatternsService: Pick + indexPatternsService: Pick, + options: { + fetchKibabaIndexForStringIndexes: boolean; + } = { + fetchKibabaIndexForStringIndexes: false, + } ): Promise => { let indexPattern: FetchedIndexPattern['indexPattern']; let indexPatternString: string = ''; @@ -61,13 +66,16 @@ export const fetchIndexPattern = async ( indexPattern = await indexPatternsService.getDefault(); } else { if (isStringTypeIndexPattern(indexPatternValue)) { - indexPattern = (await indexPatternsService.find(indexPatternValue)).find( - (index) => index.title === indexPatternValue - ); - + if (options.fetchKibabaIndexForStringIndexes) { + indexPattern = (await indexPatternsService.find(indexPatternValue)).find( + (index) => index.title === indexPatternValue + ); + } if (!indexPattern) { indexPatternString = indexPatternValue; } + + indexPatternString = indexPatternValue; } else if (indexPatternValue.id) { indexPattern = await indexPatternsService.get(indexPatternValue.id); } diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts index 74e247b7af06d..240b3e68cf65d 100644 --- a/src/plugins/vis_type_timeseries/common/types.ts +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -46,6 +46,7 @@ interface TableData { export type SeriesData = { type: Exclude; uiRestrictions: TimeseriesUIRestrictions; + error?: string; } & { [key: string]: PanelSeries; }; @@ -56,7 +57,7 @@ interface PanelSeries { }; id: string; series: PanelData[]; - error?: unknown; + error?: string; } export interface PanelData { @@ -66,6 +67,7 @@ export interface PanelData { seriesId: string; splitByLabel: string; isSplitByTerms: boolean; + error?: string; } export const isVisTableData = (data: TimeseriesVisData): data is TableData => diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx index 82989cc15d6c9..7d42eb3f40ac5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx @@ -5,19 +5,26 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox, EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui'; -import { METRIC_TYPES } from '../../../../common/metric_types'; +import React, { ReactNode, useContext } from 'react'; +import { + EuiComboBox, + EuiComboBoxProps, + EuiComboBoxOptionOption, + EuiFormRow, + htmlIdGenerator, +} from '@elastic/eui'; import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; import type { SanitizedFieldType, IndexPatternValue } from '../../../../common/types'; import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; // @ts-ignore import { isFieldEnabled } from '../../lib/check_ui_restrictions'; +import { PanelModelContext } from '../../contexts/panel_model_context'; +import { USE_KIBANA_INDEXES_KEY } from '../../../../common/constants'; interface FieldSelectProps { + label: string | ReactNode; type: string; fields: Record; indexPattern: IndexPatternValue; @@ -45,6 +52,7 @@ const sortByLabel = (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOpt }; export function FieldSelect({ + label, type, fields, indexPattern = '', @@ -56,11 +64,10 @@ export function FieldSelect({ uiRestrictions, 'data-test-subj': dataTestSubj = 'metricsIndexPatternFieldsSelect', }: FieldSelectProps) { - if (type === METRIC_TYPES.COUNT) { - return null; - } + const panelModel = useContext(PanelModelContext); + const htmlId = htmlIdGenerator(); - const selectedOptions: Array> = []; + let selectedOptions: Array> = []; let newPlaceholder = placeholder; const fieldsSelector = getIndexPatternKey(indexPattern); @@ -112,19 +119,43 @@ export function FieldSelect({ } }); - if (value && !selectedOptions.length) { - onChange([]); + let isInvalid; + + if (Boolean(panelModel?.[USE_KIBANA_INDEXES_KEY])) { + isInvalid = Boolean(value && fields[fieldsSelector] && !selectedOptions.length); + + if (value && !selectedOptions.length) { + selectedOptions = [{ label: value!, id: 'INVALID_FIELD' }]; + } + } else { + if (value && !selectedOptions.length) { + onChange([]); + } } return ( - + + + ); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 90353f9af8e35..c380b0e09e7d3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -153,24 +153,20 @@ export const FilterRatioAgg = (props) => { {model.metric_agg !== 'count' ? ( - } - > - - + fields={fields} + type={model.metric_agg} + restrict={getSupportedFieldsByMetricType(model.metric_agg)} + indexPattern={indexPattern} + value={model.field} + onChange={handleSelectChange('field')} + /> ) : null} diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js index 964017cf886ec..7ce432a3bf676 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js @@ -70,7 +70,7 @@ export function MetricSelect(props) { const percentileOptions = siblings .filter((row) => /^percentile/.test(row.type)) .reduce((acc, row) => { - const label = calculateLabel(row, calculatedMetrics, fields); + const label = calculateLabel(row, calculatedMetrics, fields, false); switch (row.type) { case METRIC_TYPES.PERCENTILE_RANK: @@ -100,7 +100,7 @@ export function MetricSelect(props) { }, []); const options = siblings.filter(filterRows(includeSiblings)).map((row) => { - const label = calculateLabel(row, calculatedMetrics, fields); + const label = calculateLabel(row, calculatedMetrics, fields, false); return { value: row.id, label }; }); const allOptions = [...options, ...additionalOptions, ...percentileOptions]; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js index 77b2e2f020307..45bb5387c5cd3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js @@ -78,24 +78,20 @@ export function PercentileAgg(props) { /> - } - > - - + fields={fields} + type={model.type} + restrict={RESTRICT_FIELDS} + indexPattern={indexPattern} + value={model.field} + onChange={handleSelectChange('field')} + /> { /> - } - > - - + fields={fields} + type={model.type} + restrict={RESTRICT_FIELDS} + indexPattern={indexPattern} + value={model.field ?? ''} + onChange={handleSelectChange('field')} + /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js index 4b1528ca27081..09d9f2f1a62f2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js @@ -99,27 +99,22 @@ export const PositiveRateAgg = (props) => { /> - } + fields={props.fields} + type={model.type} + restrict={[KBN_FIELD_TYPES.NUMBER]} + indexPattern={indexPattern} + value={model.field} + onChange={handleSelectChange('field')} + uiRestrictions={props.uiRestrictions} fullWidth - > - - + /> - } + fields={fields} + type={model.type} + restrict={restrictFields} + indexPattern={indexPattern} + value={model.field} + onChange={handleSelectChange('field')} + uiRestrictions={uiRestrictions} fullWidth - > - - + /> ) : null} diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js index 749a97fa79f28..d4caa8a94652f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js @@ -107,24 +107,20 @@ const StandardDeviationAggUi = (props) => { /> - } - > - - + fields={fields} + type={model.type} + restrict={RESTRICT_FIELDS} + indexPattern={indexPattern} + value={model.field} + onChange={handleSelectChange('field')} + /> { /> - } - > - - + fields={fields} + type={model.type} + restrict={aggWithOptionsRestrictFields} + indexPattern={indexPattern} + value={model.field} + onChange={handleSelectChange('field')} + /> @@ -223,23 +219,19 @@ const TopHitAggUi = (props) => { - } - > - - + restrict={ORDER_DATE_RESTRICT_FIELDS} + value={model.order_by} + onChange={handleSelectChange('order_by')} + indexPattern={indexPattern} + fields={fields} + /> - } + restrict={RESTRICT_FIELDS} + value={model.time_field} + onChange={this.handleChange(model, 'time_field')} + indexPattern={model.index_pattern} + fields={this.props.fields} fullWidth - > - - + /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index 5a991238d10f8..e7a34c6e6596d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -77,8 +77,8 @@ export const IndexPattern = ({ const intervalName = `${prefix}interval`; const maxBarsName = `${prefix}max_bars`; const dropBucketName = `${prefix}drop_last_bucket`; - const defaultIndex = useContext(DefaultIndexPatternContext); const updateControlValidity = useContext(FormValidationContext); + const defaultIndex = useContext(DefaultIndexPatternContext); const uiRestrictions = get(useContext(VisDataContext), 'uiRestrictions'); const maxBarsUiSettings = config.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); @@ -192,22 +192,18 @@ export const IndexPattern = ({ /> - - - + restrict={RESTRICT_FIELDS} + value={model[timeFieldName]} + disabled={disabled} + onChange={handleSelectChange(timeFieldName)} + indexPattern={model[indexPatternName]} + fields={fields} + placeholder={!model[indexPatternName] ? defaultIndex?.timeFieldName : undefined} + /> - } - > - - + fields={this.props.fields} + value={model.pivot_id} + indexPattern={model.index_pattern} + onChange={this.handlePivotChange} + uiRestrictions={this.context.uiRestrictions} + type={BUCKET_TYPES.TERMS} + /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 09cd6d550fd96..562c463f6c83c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -26,13 +26,25 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js - } - labelType="label" - > - - + onChange={[Function]} + type="terms" + value="OriginCityName" + /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index ab5342e925bd7..7db6a75e2392c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -110,8 +110,7 @@ export const SplitByTermsUI = ({ - } - > - - + data-test-subj="groupByField" + indexPattern={indexPattern} + onChange={handleSelectChange('terms_field')} + value={model.terms_field} + fields={fields} + uiRestrictions={uiRestrictions} + type={'terms'} + /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js index 0ba8d3e855365..1940ac8b2e9b9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js @@ -186,20 +186,16 @@ export class TableSeriesConfig extends Component { - } - > - - + fields={this.props.fields} + indexPattern={this.props.panel.index_pattern} + value={model.aggregate_by} + onChange={handleSelectChange('aggregate_by')} + fullWidth + /> { }); describe('text-based index', () => { - test('should return the Kibana index if it exists', async () => { - mockedIndices = [ - { - id: 'indexId', - title: 'indexTitle', - }, - ] as IndexPattern[]; - - const value = await cachedIndexPatternFetcher('indexTitle'); - - expect(value).toMatchInlineSnapshot(` - Object { - "indexPattern": Object { - "id": "indexId", - "title": "indexTitle", - }, - "indexPatternString": "indexTitle", - } - `); - }); - - test('should return only indexPatternString if Kibana index does not exist', async () => { + test('should return only indexPatternString', async () => { const value = await cachedIndexPatternFetcher('indexTitle'); expect(value).toMatchInlineSnapshot(` diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts index 9003eb7fc2ced..4b13e62430c47 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts @@ -6,10 +6,13 @@ * Side Public License, v 1. */ +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; + import type { VisTypeTimeseriesVisDataRequest } from '../../../types'; import type { AbstractSearchStrategy, DefaultSearchCapabilities } from '../index'; import type { IndexPatternsService } from '../../../../../data/common'; import type { CachedIndexPatternFetcher } from './cached_index_pattern_fetcher'; +import type { IndexPatternValue } from '../../../../common/types'; export interface FieldsFetcherServices { indexPatternsService: IndexPatternsService; @@ -29,11 +32,13 @@ export const createFieldsFetcher = ( ) => { const fieldsCacheMap = new Map(); - return async (index: string) => { - if (fieldsCacheMap.has(index)) { - return fieldsCacheMap.get(index); + return async (indexPatternValue: IndexPatternValue) => { + const key = getIndexPatternKey(indexPatternValue); + + if (fieldsCacheMap.has(key)) { + return fieldsCacheMap.get(key); } - const fetchedIndex = await cachedIndexPatternFetcher(index); + const fetchedIndex = await cachedIndexPatternFetcher(indexPatternValue); const fields = await searchStrategy.getFieldsForWildcard( fetchedIndex, @@ -41,7 +46,7 @@ export const createFieldsFetcher = ( capabilities ); - fieldsCacheMap.set(index, fields); + fieldsCacheMap.set(key, fields); return fields; }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts index 5a84598bb5ed2..1350e56b68f59 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts @@ -7,8 +7,8 @@ */ import { IUiSettingsClient } from 'kibana/server'; -import { EsQueryConfig, IndexPattern } from 'src/plugins/data/server'; -import { AnnotationItemsSchema, PanelSchema } from '../../../../common/types'; +import { EsQueryConfig } from 'src/plugins/data/server'; +import { AnnotationItemsSchema, FetchedIndexPattern, PanelSchema } from '../../../../common/types'; import { VisTypeTimeseriesVisDataRequest } from '../../../types'; import { DefaultSearchCapabilities } from '../../search_strategies'; import { buildProcessorFunction } from '../build_processor_function'; @@ -17,16 +17,6 @@ import { processors } from '../request_processors/annotations'; /** * Builds annotation request body - * - * @param {...args}: [ - * req: {Object} - a request object, - * panel: {Object} - a panel object, - * annotation: {Object} - an annotation object, - * esQueryConfig: {Object} - es query config object, - * indexPatternObject: {Object} - an index pattern object, - * capabilities: {Object} - a search capabilities object - * ] - * @returns {Object} doc - processed body */ export async function buildAnnotationRequest( ...args: [ @@ -34,7 +24,7 @@ export async function buildAnnotationRequest( PanelSchema, AnnotationItemsSchema, EsQueryConfig, - IndexPattern | null | undefined, + FetchedIndexPattern, DefaultSearchCapabilities, IUiSettingsClient ] diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts index 32086fbf4f5b4..40f1b4f2cc051 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts @@ -33,24 +33,23 @@ export async function getAnnotationRequestParams( cachedIndexPatternFetcher, }: AnnotationServices ) { - const { indexPattern, indexPatternString } = await cachedIndexPatternFetcher( - annotation.index_pattern - ); + const annotationIndex = await cachedIndexPatternFetcher(annotation.index_pattern); const request = await buildAnnotationRequest( req, panel, annotation, esQueryConfig, - indexPattern, + annotationIndex, capabilities, uiSettings ); return { - index: indexPatternString, + index: annotationIndex.indexPatternString, body: { ...request, + runtime_mappings: annotationIndex.indexPattern?.getComputedFields().runtimeFields ?? {}, timeout: esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined, }, }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts similarity index 68% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js rename to src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts index ceb867e4e6d1e..7c0a0f5deb601 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts @@ -7,25 +7,30 @@ */ import { getIntervalAndTimefield } from './get_interval_and_timefield'; +import { FetchedIndexPattern, PanelSchema, SeriesItemsSchema } from '../../../common/types'; describe('getIntervalAndTimefield(panel, series)', () => { + const index: FetchedIndexPattern = {} as FetchedIndexPattern; + test('returns the panel interval and timefield', () => { - const panel = { time_field: '@timestamp', interval: 'auto' }; - const series = {}; - expect(getIntervalAndTimefield(panel, series)).toEqual({ + const panel = { time_field: '@timestamp', interval: 'auto' } as PanelSchema; + const series = {} as SeriesItemsSchema; + + expect(getIntervalAndTimefield(panel, series, index)).toEqual({ timeField: '@timestamp', interval: 'auto', }); }); test('returns the series interval and timefield', () => { - const panel = { time_field: '@timestamp', interval: 'auto' }; - const series = { + const panel = { time_field: '@timestamp', interval: 'auto' } as PanelSchema; + const series = ({ override_index_pattern: true, series_interval: '1m', series_time_field: 'time', - }; - expect(getIntervalAndTimefield(panel, series)).toEqual({ + } as unknown) as SeriesItemsSchema; + + expect(getIntervalAndTimefield(panel, series, index)).toEqual({ timeField: 'time', interval: '1m', }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts similarity index 57% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js rename to src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts index ebab984ff25aa..e3d0cec1a6939 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts @@ -7,28 +7,31 @@ */ import { AUTO_INTERVAL } from '../../../common/constants'; +import { FetchedIndexPattern, PanelSchema, SeriesItemsSchema } from '../../../common/types'; +import { validateField } from '../../../common/fields_utils'; -const DEFAULT_TIME_FIELD = '@timestamp'; - -export function getIntervalAndTimefield(panel, series = {}, indexPattern) { - const getDefaultTimeField = () => indexPattern?.timeFieldName ?? DEFAULT_TIME_FIELD; - +export function getIntervalAndTimefield( + panel: PanelSchema, + series: SeriesItemsSchema, + index: FetchedIndexPattern +) { const timeField = - (series.override_index_pattern && series.series_time_field) || - panel.time_field || - getDefaultTimeField(); + (series.override_index_pattern ? series.series_time_field : panel.time_field) || + index.indexPattern?.timeFieldName; + + validateField(timeField!, index); let interval = panel.interval; let maxBars = panel.max_bars; if (series.override_index_pattern) { - interval = series.series_interval; + interval = series.series_interval || AUTO_INTERVAL; maxBars = series.series_max_bars; } return { + maxBars, timeField, interval: interval || AUTO_INTERVAL, - maxBars, }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts index 0cc1188086b7b..b50fdb6b8226d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts @@ -18,7 +18,7 @@ import { handleErrorResponse } from './handle_error_response'; import { processBucket } from './table/process_bucket'; import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher'; -import { extractFieldLabel } from '../../../common/calculate_label'; +import { extractFieldLabel } from '../../../common/fields_utils'; import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequestServices, @@ -58,8 +58,8 @@ export async function getTableData( }); const calculatePivotLabel = async () => { - if (panel.pivot_id && panelIndex.indexPattern?.title) { - const fields = await extractFields(panelIndex.indexPattern.title); + if (panel.pivot_id && panelIndex.indexPattern?.id) { + const fields = await extractFields({ id: panelIndex.indexPattern.id }); return extractFieldLabel(fields, panel.pivot_id); } @@ -68,7 +68,6 @@ export async function getTableData( const meta = { type: panel.type, - pivot_label: panel.pivot_label || (await calculatePivotLabel()), uiRestrictions: capabilities.uiRestrictions, }; @@ -77,14 +76,17 @@ export async function getTableData( req, panel, services.esQueryConfig, - panelIndex.indexPattern, + panelIndex, capabilities, services.uiSettings ); const [resp] = await searchStrategy.search(requestContext, req, [ { - body, + body: { + ...body, + runtime_mappings: panelIndex.indexPattern?.getComputedFields().runtimeFields ?? {}, + }, index: panelIndex.indexPatternString, }, ]); @@ -101,6 +103,7 @@ export async function getTableData( return { ...meta, + pivot_label: panel.pivot_label || (await calculatePivotLabel()), series, }; } catch (err) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js index 268c26115233e..27e7c5c908b9a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js @@ -23,9 +23,8 @@ export async function getSplits(resp, panel, series, meta, extractFields) { const color = new Color(series.color); const metric = getLastMetric(series); const buckets = _.get(resp, `aggregations.${series.id}.buckets`); - - const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : []; - const splitByLabel = calculateLabel(metric, series.metrics, fieldsForMetaIndex); + const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : []; + const splitByLabel = calculateLabel(metric, series.metrics, fieldsForSeries); if (buckets) { if (Array.isArray(buckets)) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js index 22a475a9997a7..f3ee416be81a8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js @@ -10,6 +10,7 @@ import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimerange } from '../../helpers/get_timerange'; import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; +import { validateField } from '../../../../../common/fields_utils'; const { dateHistogramInterval } = search.aggs; @@ -18,13 +19,16 @@ export function dateHistogram( panel, annotation, esQueryConfig, - indexPattern, + annotationIndex, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const timeField = annotation.time_field; + + validateField(timeField, annotationIndex); + const { bucketSize, intervalString } = getBucketSize( req, 'auto', diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js index e7270371a3fdc..46a3c369e548d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js @@ -9,26 +9,30 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimerange } from '../../helpers/get_timerange'; import { esQuery, UI_SETTINGS } from '../../../../../../data/server'; +import { validateField } from '../../../../../common/fields_utils'; export function query( req, panel, annotation, esQueryConfig, - indexPattern, + annotationIndex, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const timeField = annotation.time_field; + const timeField = (annotation.time_field || annotationIndex.indexPattern?.timeField) ?? ''; + + validateField(timeField, annotationIndex); + const { bucketSize } = getBucketSize(req, 'auto', capabilities, barTargetUiSettings); const { from, to } = getTimerange(req); doc.size = 0; const queries = !annotation.ignore_global_filters ? req.body.query : []; const filters = !annotation.ignore_global_filters ? req.body.filters : []; - doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig); + doc.query = esQuery.buildEsQuery(annotationIndex.indexPattern, queries, filters, esQueryConfig); const timerange = { range: { [timeField]: { @@ -42,13 +46,18 @@ export function query( if (annotation.query_string) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPattern, [annotation.query_string], [], esQueryConfig) + esQuery.buildEsQuery( + annotationIndex.indexPattern, + [annotation.query_string], + [], + esQueryConfig + ) ); } if (!annotation.ignore_panel_filters && panel.filter) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig) + esQuery.buildEsQuery(annotationIndex.indexPattern, [panel.filter], [], esQueryConfig) ); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js index 2e759cb6b8b74..1b4434c4867c8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js @@ -7,12 +7,15 @@ */ import { overwrite } from '../../helpers'; +import { validateField } from '../../../../../common/fields_utils'; -export function topHits(req, panel, annotation) { +export function topHits(req, panel, annotation, esQueryConfig, annotationIndex) { return (next) => (doc) => { const fields = (annotation.fields && annotation.fields.split(/[,\s]+/)) || []; const timeField = annotation.time_field; + validateField(timeField, annotationIndex); + overwrite(doc, `aggs.${annotation.id}.aggs.hits.top_hits`, { sort: [ { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index a9b4f99fdb693..41ed472c31936 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -12,6 +12,7 @@ import { offsetTime } from '../../offset_time'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; + const { dateHistogramInterval } = search.aggs; export function dateHistogram( @@ -19,7 +20,7 @@ export function dateHistogram( panel, series, esQueryConfig, - indexPattern, + seriesIndex, capabilities, uiSettings ) { @@ -27,7 +28,7 @@ export function dateHistogram( const maxBarsUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { timeField, interval, maxBars } = getIntervalAndTimefield(panel, series, indexPattern); + const { timeField, interval, maxBars } = getIntervalAndTimefield(panel, series, seriesIndex); const { bucketSize, intervalString } = getBucketSize( req, interval, @@ -64,9 +65,9 @@ export function dateHistogram( overwrite(doc, `aggs.${series.id}.meta`, { timeField, intervalString, - index: indexPattern?.title, bucketSize, seriesId: series.id, + index: seriesIndex.indexPattern?.id, }); return next(doc); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js index 4639af9db83b8..d45943f6f21ac 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js @@ -12,19 +12,19 @@ import { esQuery } from '../../../../../../data/server'; const filter = (metric) => metric.type === 'filter_ratio'; -export function ratios(req, panel, series, esQueryConfig, indexPattern) { +export function ratios(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { if (series.metrics.some(filter)) { series.metrics.filter(filter).forEach((metric) => { overwrite( doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.filter`, - esQuery.buildEsQuery(indexPattern, metric.numerator, [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, metric.numerator, [], esQueryConfig) ); overwrite( doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.filter`, - esQuery.buildEsQuery(indexPattern, metric.denominator, [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, metric.denominator, [], esQueryConfig) ); let numeratorPath = `${metric.id}-numerator>_count`; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js index 345488ec01d5e..a93827ba82cd6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js @@ -8,7 +8,7 @@ import { ratios } from './filter_ratios'; -describe('ratios(req, panel, series, esQueryConfig, indexPatternObject)', () => { +describe('ratios(req, panel, series, esQueryConfig, seriesIndex)', () => { let panel; let series; let req; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js index 86b691f6496c9..29a11bf163e0b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js @@ -17,14 +17,14 @@ export function metricBuckets( panel, series, esQueryConfig, - indexPattern, + seriesIndex, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, series, indexPattern); + const { interval } = getIntervalAndTimefield(panel, series, seriesIndex); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); series.metrics diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js index ce61374c0b124..208321a98737e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -56,14 +56,14 @@ export function positiveRate( panel, series, esQueryConfig, - indexPattern, + seriesIndex, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, series, indexPattern); + const { interval } = getIntervalAndTimefield(panel, series, seriesIndex); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); if (series.metrics.some(filter)) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js index d0e92c9157cb5..a5f4e17289e06 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js @@ -10,16 +10,16 @@ import { offsetTime } from '../../offset_time'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { esQuery } from '../../../../../../data/server'; -export function query(req, panel, series, esQueryConfig, indexPattern) { +export function query(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { - const { timeField } = getIntervalAndTimefield(panel, series, indexPattern); + const { timeField } = getIntervalAndTimefield(panel, series, seriesIndex); const { from, to } = offsetTime(req, series.offset_time); doc.size = 0; const ignoreGlobalFilter = panel.ignore_global_filter || series.ignore_global_filter; const queries = !ignoreGlobalFilter ? req.body.query : []; const filters = !ignoreGlobalFilter ? req.body.filters : []; - doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig); + doc.query = esQuery.buildEsQuery(seriesIndex.indexPattern, queries, filters, esQueryConfig); const timerange = { range: { @@ -34,13 +34,13 @@ export function query(req, panel, series, esQueryConfig, indexPattern) { if (panel.filter) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, [panel.filter], [], esQueryConfig) ); } if (series.filter) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPattern, [series.filter], [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, [series.filter], [], esQueryConfig) ); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js index 2772aed822517..b3e88dbf1c6b9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js @@ -8,15 +8,17 @@ import { query } from './query'; -describe('query(req, panel, series)', () => { +describe('query', () => { let panel; let series; let req; + let seriesIndex; const config = { allowLeadingWildcards: true, queryStringOptions: { analyze_wildcard: true }, }; + beforeEach(() => { req = { body: { @@ -32,17 +34,18 @@ describe('query(req, panel, series)', () => { interval: '10s', }; series = { id: 'test' }; + seriesIndex = {}; }); test('calls next when finished', () => { const next = jest.fn(); - query(req, panel, series, config)(next)({}); + query(req, panel, series, config, seriesIndex)(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns doc with query for timerange', () => { const next = (doc) => doc; - const doc = query(req, panel, series, config)(next)({}); + const doc = query(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ size: 0, query: { @@ -69,7 +72,7 @@ describe('query(req, panel, series)', () => { test('returns doc with query for timerange (offset by 1h)', () => { series.offset_time = '1h'; const next = (doc) => doc; - const doc = query(req, panel, series, config)(next)({}); + const doc = query(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ size: 0, query: { @@ -108,7 +111,7 @@ describe('query(req, panel, series)', () => { }, ]; const next = (doc) => doc; - const doc = query(req, panel, series, config)(next)({}); + const doc = query(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ size: 0, query: { @@ -147,7 +150,7 @@ describe('query(req, panel, series)', () => { test('returns doc with series filter', () => { series.filter = { query: 'host:web-server', language: 'lucene' }; const next = (doc) => doc; - const doc = query(req, panel, series, config)(next)({}); + const doc = query(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ size: 0, query: { @@ -201,7 +204,7 @@ describe('query(req, panel, series)', () => { ]; panel.filter = { query: 'host:web-server', language: 'lucene' }; const next = (doc) => doc; - const doc = query(req, panel, series, config)(next)({}); + const doc = query(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ size: 0, query: { @@ -269,7 +272,7 @@ describe('query(req, panel, series)', () => { panel.filter = { query: 'host:web-server', language: 'lucene' }; panel.ignore_global_filter = true; const next = (doc) => doc; - const doc = query(req, panel, series, config)(next)({}); + const doc = query(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ size: 0, query: { @@ -325,7 +328,7 @@ describe('query(req, panel, series)', () => { panel.filter = { query: 'host:web-server', language: 'lucene' }; series.ignore_global_filter = true; const next = (doc) => doc; - const doc = query(req, panel, series, config)(next)({}); + const doc = query(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ size: 0, query: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js index 401344d48f865..dbeb3b1393bd5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js @@ -17,13 +17,13 @@ export function siblingBuckets( panel, series, esQueryConfig, - indexPattern, + seriesIndex, capabilities, uiSettings ) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, series, indexPattern); + const { interval } = getIntervalAndTimefield(panel, series, seriesIndex); const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings); series.metrics diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js index 25d62d4f7fe07..01e1b9f8d1dce 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js @@ -9,7 +9,7 @@ import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; -export function splitByFilter(req, panel, series, esQueryConfig, indexPattern) { +export function splitByFilter(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { if (series.split_mode !== 'filter') { return next(doc); @@ -18,7 +18,7 @@ export function splitByFilter(req, panel, series, esQueryConfig, indexPattern) { overwrite( doc, `aggs.${series.id}.filter`, - esQuery.buildEsQuery(indexPattern, [series.filter], [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, [series.filter], [], esQueryConfig) ); return next(doc); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js index ad6e84dbc7842..9722833837167 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js @@ -12,8 +12,12 @@ describe('splitByFilter(req, panel, series)', () => { let panel; let series; let req; + let config; + let seriesIndex; + beforeEach(() => { panel = {}; + config = {}; series = { id: 'test', split_mode: 'filter', @@ -27,17 +31,18 @@ describe('splitByFilter(req, panel, series)', () => { }, }, }; + seriesIndex = {}; }); test('calls next when finished', () => { const next = jest.fn(); - splitByFilter(req, panel, series)(next)({}); + splitByFilter(req, panel, series, config, seriesIndex)(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns a valid filter with a query_string', () => { const next = (doc) => doc; - const doc = splitByFilter(req, panel, series)(next)({}); + const doc = splitByFilter(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ aggs: { test: { @@ -63,7 +68,7 @@ describe('splitByFilter(req, panel, series)', () => { test('calls next and does not add a filter', () => { series.split_mode = 'terms'; const next = jest.fn((doc) => doc); - const doc = splitByFilter(req, panel, series)(next)({}); + const doc = splitByFilter(req, panel, series, config, seriesIndex)(next)({}); expect(next.mock.calls.length).toEqual(1); expect(doc).toEqual({}); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js index 237ed16e5a8b6..77b9ccc5880fe 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js @@ -9,11 +9,16 @@ import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; -export function splitByFilters(req, panel, series, esQueryConfig, indexPattern) { +export function splitByFilters(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { if (series.split_mode === 'filters' && series.split_filters) { series.split_filters.forEach((filter) => { - const builtEsQuery = esQuery.buildEsQuery(indexPattern, [filter.filter], [], esQueryConfig); + const builtEsQuery = esQuery.buildEsQuery( + seriesIndex.indexPattern, + [filter.filter], + [], + esQueryConfig + ); overwrite(doc, `aggs.${series.id}.filters.filters.${filter.id}`, builtEsQuery); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js index fdcdfe45d2fd2..2a44bf2538a4b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js @@ -12,7 +12,11 @@ describe('splitByFilters(req, panel, series)', () => { let panel; let series; let req; + let config; + let seriesIndex; + beforeEach(() => { + config = {}; panel = { time_field: 'timestamp', }; @@ -43,17 +47,18 @@ describe('splitByFilters(req, panel, series)', () => { }, }, }; + seriesIndex = {}; }); test('calls next when finished', () => { const next = jest.fn(); - splitByFilters(req, panel, series)(next)({}); + splitByFilters(req, panel, series, config, seriesIndex)(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns a valid terms agg', () => { const next = (doc) => doc; - const doc = splitByFilters(req, panel, series)(next)({}); + const doc = splitByFilters(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ aggs: { test: { @@ -97,7 +102,7 @@ describe('splitByFilters(req, panel, series)', () => { test('calls next and does not add a terms agg', () => { series.split_mode = 'everything'; const next = jest.fn((doc) => doc); - const doc = splitByFilters(req, panel, series)(next)({}); + const doc = splitByFilters(req, panel, series, config, seriesIndex)(next)({}); expect(next.mock.calls.length).toEqual(1); expect(doc).toEqual({}); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js index 8f72bd2d12951..9c2bdbe03f886 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js @@ -10,13 +10,16 @@ import { overwrite } from '../../helpers'; import { basicAggs } from '../../../../../common/basic_aggs'; import { getBucketsPath } from '../../helpers/get_buckets_path'; import { bucketTransform } from '../../helpers/bucket_transform'; +import { validateField } from '../../../../../common/fields_utils'; -export function splitByTerms(req, panel, series) { +export function splitByTerms(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { if (series.split_mode === 'terms' && series.terms_field) { const termsField = series.terms_field; const orderByTerms = series.terms_order_by; + validateField(termsField, seriesIndex); + const direction = series.terms_direction || 'desc'; const metric = series.metrics.find((item) => item.id === orderByTerms); overwrite(doc, `aggs.${series.id}.terms.field`, termsField); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js index 37d188c00eee3..984eb385ca4a6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js @@ -8,11 +8,18 @@ import { splitByTerms } from './split_by_terms'; -describe('splitByTerms(req, panel, series)', () => { +describe('splitByTerms', () => { let panel; let series; let req; + let config; + let seriesIndex; + beforeEach(() => { + config = { + allowLeadingWildcards: true, + queryStringOptions: { analyze_wildcard: true }, + }; panel = { time_field: 'timestamp', }; @@ -31,17 +38,18 @@ describe('splitByTerms(req, panel, series)', () => { }, }, }; + seriesIndex = {}; }); test('calls next when finished', () => { const next = jest.fn(); - splitByTerms(req, panel, series)(next)({}); + splitByTerms(req, panel, series, config, seriesIndex)(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns a valid terms agg', () => { const next = (doc) => doc; - const doc = splitByTerms(req, panel, series)(next)({}); + const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ aggs: { test: { @@ -61,7 +69,7 @@ describe('splitByTerms(req, panel, series)', () => { const next = (doc) => doc; series.terms_order_by = '_key'; series.terms_direction = 'asc'; - const doc = splitByTerms(req, panel, series)(next)({}); + const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ aggs: { test: { @@ -80,7 +88,7 @@ describe('splitByTerms(req, panel, series)', () => { test('returns a valid terms agg with custom sort', () => { series.terms_order_by = 'avgmetric'; const next = (doc) => doc; - const doc = splitByTerms(req, panel, series)(next)({}); + const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({}); expect(doc).toEqual({ aggs: { test: { @@ -106,7 +114,7 @@ describe('splitByTerms(req, panel, series)', () => { test('calls next and does not add a terms agg', () => { series.split_mode = 'everything'; const next = jest.fn((doc) => doc); - const doc = splitByTerms(req, panel, series)(next)({}); + const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({}); expect(next.mock.calls.length).toEqual(1); expect(doc).toEqual({}); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index aff1bd5041be5..4840e625383ca 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -13,15 +13,17 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { getTimerange } from '../../helpers/get_timerange'; import { calculateAggRoot } from './calculate_agg_root'; import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; + const { dateHistogramInterval } = search.aggs; -export function dateHistogram(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { +export function dateHistogram(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPattern); + const { timeField, interval } = getIntervalAndTimefield(panel, {}, seriesIndex); + const meta = { timeField, - index: indexPattern?.title, + index: seriesIndex.indexPattern?.id, }; const getDateHistogramForLastBucketMode = () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js index abb5971908771..e15330334639f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js @@ -13,7 +13,7 @@ import { calculateAggRoot } from './calculate_agg_root'; const filter = (metric) => metric.type === 'filter_ratio'; -export function ratios(req, panel, esQueryConfig, indexPattern) { +export function ratios(req, panel, esQueryConfig, seriesIndex) { return (next) => (doc) => { panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); @@ -22,12 +22,12 @@ export function ratios(req, panel, esQueryConfig, indexPattern) { overwrite( doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, - esQuery.buildEsQuery(indexPattern, metric.numerator, [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, metric.numerator, [], esQueryConfig) ); overwrite( doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, - esQuery.buildEsQuery(indexPattern, metric.denominator, [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, metric.denominator, [], esQueryConfig) ); let numeratorPath = `${metric.id}-numerator>_count`; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js index 5ce508bd9b279..421f9d2d75f0c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js @@ -13,10 +13,10 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; import { UI_SETTINGS } from '../../../../../../data/common'; -export function metricBuckets(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { +export function metricBuckets(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, {}, indexPattern); + const { interval } = getIntervalAndTimefield(panel, {}, seriesIndex); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); panel.series.forEach((column) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js index 176721e7b563a..3390362b56115 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js @@ -12,10 +12,10 @@ import { calculateAggRoot } from './calculate_agg_root'; import { createPositiveRate, filter } from '../series/positive_rate'; import { UI_SETTINGS } from '../../../../../../data/common'; -export function positiveRate(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { +export function positiveRate(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, {}, indexPattern); + const { interval } = getIntervalAndTimefield(panel, {}, seriesIndex); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); panel.series.forEach((column) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js index 76df07b76e80e..66783e0cdfaef 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js @@ -10,16 +10,16 @@ import { getTimerange } from '../../helpers/get_timerange'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { esQuery } from '../../../../../../data/server'; -export function query(req, panel, esQueryConfig, indexPattern) { +export function query(req, panel, esQueryConfig, seriesIndex) { return (next) => (doc) => { - const { timeField } = getIntervalAndTimefield(panel, {}, indexPattern); + const { timeField } = getIntervalAndTimefield(panel, {}, seriesIndex); const { from, to } = getTimerange(req); doc.size = 0; const queries = !panel.ignore_global_filter ? req.body.query : []; const filters = !panel.ignore_global_filter ? req.body.filters : []; - doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig); + doc.query = esQuery.buildEsQuery(seriesIndex.indexPattern, queries, filters, esQueryConfig); const timerange = { range: { @@ -33,7 +33,7 @@ export function query(req, panel, esQueryConfig, indexPattern) { doc.query.bool.must.push(timerange); if (panel.filter) { doc.query.bool.must.push( - esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, [panel.filter], [], esQueryConfig) ); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js index 5539f16df41e0..9b4b0f244fc2c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js @@ -13,10 +13,10 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; import { UI_SETTINGS } from '../../../../../../data/common'; -export function siblingBuckets(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) { +export function siblingBuckets(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) { return (next) => async (doc) => { const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); - const { interval } = getIntervalAndTimefield(panel, {}, indexPattern); + const { interval } = getIntervalAndTimefield(panel, {}, seriesIndex); const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings); panel.series.forEach((column) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js index 595d49ebbd836..cda022294507f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js @@ -9,7 +9,7 @@ import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; -export function splitByEverything(req, panel, esQueryConfig, indexPattern) { +export function splitByEverything(req, panel, esQueryConfig, seriesIndex) { return (next) => (doc) => { panel.series .filter((c) => !(c.aggregate_by && c.aggregate_function)) @@ -18,7 +18,7 @@ export function splitByEverything(req, panel, esQueryConfig, indexPattern) { overwrite( doc, `aggs.pivot.aggs.${column.id}.filter`, - esQuery.buildEsQuery(indexPattern, [column.filter], [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, [column.filter], [], esQueryConfig) ); } else { overwrite(doc, `aggs.pivot.aggs.${column.id}.filter.match_all`, {}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js index b4e07455be0fb..b3afc334ac2dd 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js @@ -9,7 +9,7 @@ import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; -export function splitByTerms(req, panel, esQueryConfig, indexPattern) { +export function splitByTerms(req, panel, esQueryConfig, seriesIndex) { return (next) => (doc) => { panel.series .filter((c) => c.aggregate_by && c.aggregate_function) @@ -21,7 +21,7 @@ export function splitByTerms(req, panel, esQueryConfig, indexPattern) { overwrite( doc, `aggs.pivot.aggs.${column.id}.column_filter.filter`, - esQuery.buildEsQuery(indexPattern, [column.filter], [], esQueryConfig) + esQuery.buildEsQuery(seriesIndex.indexPattern, [column.filter], [], esQueryConfig) ); } }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js index ba0271ba286a1..a803439c7581f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js @@ -5,9 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { last, first } from 'lodash'; import { SeriesAgg } from './_series_agg'; -import _ from 'lodash'; import { getDefaultDecoration } from '../../helpers/get_default_decoration'; import { calculateLabel } from '../../../../../common/calculate_label'; @@ -33,15 +32,14 @@ export function seriesAgg(resp, panel, series, meta, extractFields) { return (fn && fn(acc)) || acc; }, targetSeries); - const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : []; + const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : []; results.push({ id: `${series.id}`, label: - series.label || - calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex), + series.label || calculateLabel(last(series.metrics), series.metrics, fieldsForSeries), color: series.color, - data: _.first(data), + data: first(data), ...decoration, }); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js index 9af05afd41182..ae4968e007b1d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js @@ -7,7 +7,7 @@ */ import { SeriesAgg } from './_series_agg'; -import _ from 'lodash'; +import { last, first } from 'lodash'; import { calculateLabel } from '../../../../../common/calculate_label'; export function seriesAgg(resp, panel, series, meta, extractFields) { @@ -25,15 +25,13 @@ export function seriesAgg(resp, panel, series, meta, extractFields) { }); const fn = SeriesAgg[series.aggregate_function]; const data = fn(targetSeries); - - const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : []; + const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : []; results.push({ id: `${series.id}`, label: - series.label || - calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex), - data: _.first(data), + series.label || calculateLabel(last(series.metrics), series.metrics, fieldsForSeries), + data: first(data), }); } return next(results); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts index bab3abe13bcb0..bc046cbdcf8aa 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts @@ -18,7 +18,7 @@ import { processors } from '../request_processors/series/index'; * panel: {Object} - a panel object, * series: {Object} - an series object, * esQueryConfig: {Object} - es query config object, - * indexPatternObject: {Object} - an index pattern object, + * seriesIndex: {Object} - an index pattern object, * capabilities: {Object} - a search capabilities object * ] * @returns {Object} doc - processed body diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts index 1f2735da8fb06..827df30dacf6d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts @@ -39,7 +39,7 @@ export async function getSeriesRequestParams( panel, series, esQueryConfig, - seriesIndex.indexPattern, + seriesIndex, capabilities, uiSettings ); @@ -48,6 +48,7 @@ export async function getSeriesRequestParams( index: seriesIndex.indexPatternString, body: { ...request, + runtime_mappings: seriesIndex.indexPattern?.getComputedFields().runtimeFields ?? {}, timeout: esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined, }, }; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap index 56f35ae021173..59a7cf966df91 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap @@ -54,6 +54,7 @@ exports[`ChartOptions component should init with the default set of props 1`] = { expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Normal); }); + + it('should set "stacked" mode and disabled control if the referenced axis is "percentage"', () => { + defaultProps.valueAxes[0].scale.mode = AxisMode.Percentage; + defaultProps.chart.mode = ChartMode.Normal; + const paramName = 'mode'; + const comp = mount(); + + expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Stacked); + expect(comp.find({ paramName }).prop('disabled')).toBeTruthy(); + }); }); diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx index 6f0b4fc5c9d22..23452a87aae60 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { SelectOption } from '../../../../../../vis_default_editor/public'; -import { SeriesParam, ValueAxis } from '../../../../types'; +import { SeriesParam, ValueAxis, ChartMode, AxisMode } from '../../../../types'; import { LineOptions } from './line_options'; import { SetParamByIndex, ChangeValueAxis } from '.'; import { ChartType } from '../../../../../common'; @@ -38,6 +38,7 @@ function ChartOptions({ changeValueAxis, setParamByIndex, }: ChartOptionsParams) { + const [disabledMode, setDisabledMode] = useState(false); const setChart: SetChart = useCallback( (paramName, value) => { setParamByIndex('seriesParams', index, paramName, value); @@ -68,6 +69,20 @@ function ChartOptions({ [valueAxes] ); + useEffect(() => { + const valueAxisToMetric = valueAxes.find((valueAxis) => valueAxis.id === chart.valueAxis); + if (valueAxisToMetric) { + if (valueAxisToMetric.scale.mode === AxisMode.Percentage) { + setDisabledMode(true); + if (chart.mode !== ChartMode.Stacked) { + setChart('mode', ChartMode.Stacked); + } + } else if (disabledMode) { + setDisabledMode(false); + } + } + }, [valueAxes, chart, disabledMode, setChart, setDisabledMode]); + return ( <> diff --git a/src/plugins/visualizations/public/components/visualization_container.tsx b/src/plugins/visualizations/public/components/visualization_container.tsx index 3081c39530d75..063715b6438eb 100644 --- a/src/plugins/visualizations/public/components/visualization_container.tsx +++ b/src/plugins/visualizations/public/components/visualization_container.tsx @@ -10,6 +10,7 @@ import React, { ReactNode, Suspense } from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import classNames from 'classnames'; import { VisualizationNoResults } from './visualization_noresults'; +import { VisualizationError } from './visualization_error'; import { IInterpreterRenderHandlers } from '../../../expressions/common'; interface VisualizationContainerProps { @@ -18,6 +19,7 @@ interface VisualizationContainerProps { children: ReactNode; handlers: IInterpreterRenderHandlers; showNoResult?: boolean; + error?: string; } export const VisualizationContainer = ({ @@ -26,6 +28,7 @@ export const VisualizationContainer = ({ children, handlers, showNoResult = false, + error, }: VisualizationContainerProps) => { const classes = classNames('visualization', className); @@ -38,7 +41,13 @@ export const VisualizationContainer = ({ return (
- {showNoResult ? handlers.done()} /> : children} + {error ? ( + handlers.done()} error={error} /> + ) : showNoResult ? ( + handlers.done()} /> + ) : ( + children + )}
); diff --git a/src/plugins/visualizations/public/components/visualization_error.tsx b/src/plugins/visualizations/public/components/visualization_error.tsx new file mode 100644 index 0000000000000..81600a4e3601c --- /dev/null +++ b/src/plugins/visualizations/public/components/visualization_error.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; + +interface VisualizationNoResultsProps { + onInit?: () => void; + error: string; +} + +export class VisualizationError extends React.Component { + public render() { + return ( + {this.props.error}

} + /> + ); + } + + public componentDidMount() { + this.afterRender(); + } + + public componentDidUpdate() { + this.afterRender(); + } + + private afterRender() { + if (this.props.onInit) { + this.props.onInit(); + } + } +} diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index 87cdf5a8b0c46..c02ce76340da8 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -324,7 +324,7 @@ export default function ({ getService }: FtrProviderContext) { references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - name: 'panel_0', + name: '1:panel_1', type: 'visualization', }, ], @@ -384,7 +384,7 @@ export default function ({ getService }: FtrProviderContext) { references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - name: 'panel_0', + name: '1:panel_1', type: 'visualization', }, ], @@ -449,7 +449,7 @@ export default function ({ getService }: FtrProviderContext) { references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - name: 'panel_0', + name: '1:panel_1', type: 'visualization', }, ], diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 896cd4ad595c9..bc259c71b47b4 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); const browser = getService('browser'); - describe('discover data grid context tests', () => { + // FLAKY: https://github.com/elastic/kibana/issues/94545 + describe.skip('discover data grid context tests', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -109,7 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await alert?.accept(); expect(await browser.getCurrentUrl()).to.contain('#/context'); await PageObjects.header.waitUntilLoadingHasFinished(); - expect(await docTable.getRowsText()).to.have.length(6); + expect(await docTable.getBodyRows()).to.have.length(6); }); }); } diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 6f305d30aab46..8babba146d7ff 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -175,7 +175,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('query #2, which has an empty time range', () => { + // FLAKY: https://github.com/elastic/kibana/issues/89550 + describe.skip('query #2, which has an empty time range', () => { const fromTime = 'Jun 11, 1999 @ 09:22:11.000'; const toTime = 'Jun 12, 1999 @ 11:21:04.000'; diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 72deb74459ab9..e41422555f81d 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -21,9 +21,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; const testSubjects = getService('testSubjects'); const browser = getService('browser'); + const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/94532 - describe.skip('discover histogram', function describeIndexTests() { + describe('discover histogram', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); @@ -107,8 +107,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); await testSubjects.click('discoverChartToggle'); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(true); + await retry.waitFor(`Discover histogram to be displayed`, async () => { + canvasExists = await elasticChart.canvasExists(); + return canvasExists; + }); + await PageObjects.discover.saveSearch('persisted hidden histogram'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts index 8cb39feb2e6bb..b3e63e482e734 100644 --- a/test/functional/apps/discover/_huge_fields.ts +++ b/test/functional/apps/discover/_huge_fields.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - describe('test large number of fields in sidebar', function () { + // FLAKY: https://github.com/elastic/kibana/issues/96113 + describe.skip('test large number of fields in sidebar', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); await esArchiver.loadIfNeeded('large_fields'); diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts new file mode 100644 index 0000000000000..729ad08db81aa --- /dev/null +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from './ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const fieldEditor = getService('fieldEditor'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultSettings = { + defaultIndex: 'logstash-*', + 'discover:searchFieldsFromSource': false, + }; + describe('discover integration with runtime fields editor', function describeIndexTests() { + before(async function () { + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + log.debug('discover'); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + after(async () => { + await kibanaServer.uiSettings.replace({ 'discover:searchFieldsFromSource': true }); + }); + + it('allows adding custom label to existing fields', async function () { + await PageObjects.discover.clickFieldListItemAdd('bytes'); + await PageObjects.discover.editField('bytes'); + await fieldEditor.enableCustomLabel(); + await fieldEditor.setCustomLabel('megabytes'); + await fieldEditor.save(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.discover.getDocHeader()).to.have.string('megabytes'); + expect((await PageObjects.discover.getAllFieldNames()).includes('megabytes')).to.be(true); + }); + }); +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index e526cdaccbd4c..db76cd1c20c38 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -47,6 +47,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_data_grid_doc_navigation')); loadTestFile(require.resolve('./_data_grid_doc_table')); loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields')); + loadTestFile(require.resolve('./_runtime_fields_editor')); loadTestFile(require.resolve('./_huge_fields')); }); } diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index 8db11052d5ed0..306d251629396 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const es = getService('legacyEs'); - const PageObjects = getPageObjects(['settings', 'common']); + const PageObjects = getPageObjects(['settings', 'common', 'header']); const security = getService('security'); describe('"Create Index Pattern" wizard', function () { @@ -60,6 +60,12 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.createIndexPattern('alias1', false); }); + it('can delete an index pattern', async () => { + await PageObjects.settings.removeIndexPattern(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.exists('indexPatternTable'); + }); + after(async () => { await es.transport.request({ path: '/_aliases', diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 9a60766b839fd..90284e14b8218 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -262,6 +262,14 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider .map((field) => $(field).text()); } + public async editField(field: string) { + await retry.try(async () => { + await testSubjects.click(`field-${field}`); + await testSubjects.click(`discoverFieldListPanelEdit-${field}`); + await find.byClassName('indexPatternFieldEditor__form'); + }); + } + public async hasNoResults() { return await testSubjects.exists('discoverNoResults'); } diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts index 7d6dad4f7858e..342e2afec28d3 100644 --- a/test/functional/services/field_editor.ts +++ b/test/functional/services/field_editor.ts @@ -16,6 +16,12 @@ export function FieldEditorProvider({ getService }: FtrProviderContext) { public async setName(name: string) { await testSubjects.setValue('nameField > input', name); } + public async enableCustomLabel() { + await testSubjects.setEuiSwitch('customLabelRow > toggle', 'check'); + } + public async setCustomLabel(name: string) { + await testSubjects.setValue('customLabelRow > input', name); + } public async enableValue() { await testSubjects.setEuiSwitch('valueRow > toggle', 'check'); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index f6ec2fb24018f..78c8f151b82d9 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -6,12 +6,10 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useBreakPoints } from '../../../hooks/use_break_points'; import { LatencyChart } from '../../shared/charts/latency_chart'; @@ -46,22 +44,12 @@ export function ServiceOverview({ // observe the window width and set the flex directions of rows accordingly const { isMedium } = useBreakPoints(); const rowDirection = isMedium ? 'column' : 'row'; - - const { transactionType } = useApmServiceContext(); - const transactionTypeLabel = i18n.translate( - 'xpack.apm.serviceOverview.searchBar.transactionTypeLabel', - { defaultMessage: 'Type: {transactionType}', values: { transactionType } } - ); const isRumAgent = isRumAgentName(agentName); return ( - + diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index 6d7edcd0a1e35..364266d277482 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -49,7 +49,7 @@ export function TraceOverview() { return ( <> - + diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 0a322cfc9c80b..d6f45a4a45cc8 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -95,7 +95,7 @@ export function TransactionDetails({

{transactionName}

- + diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 0814c6d95b96a..9e2743d7b5986 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -9,7 +9,6 @@ import { EuiCallOut, EuiCode, EuiFlexGroup, - EuiFlexItem, EuiPage, EuiPanel, EuiSpacer, @@ -28,7 +27,6 @@ import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { SearchBar } from '../../shared/search_bar'; -import { TransactionTypeSelect } from '../../shared/transaction_type_select'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { useTransactionListFetcher } from './use_transaction_list'; @@ -82,33 +80,9 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { return ( <> - + - - - - - - -

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

-
-
- - - -
- -
-
diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index aeb2a2c6390fc..ed9a196bbcd9d 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -20,6 +20,7 @@ import { TimeComparison } from './time_comparison'; import { useBreakPoints } from '../../hooks/use_break_points'; import { useKibanaUrl } from '../../hooks/useKibanaUrl'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; +import { TransactionTypeSelect } from './transaction_type_select'; const EuiFlexGroupSpaced = euiStyled(EuiFlexGroup)` margin: ${({ theme }) => @@ -29,7 +30,7 @@ const EuiFlexGroupSpaced = euiStyled(EuiFlexGroup)` interface Props { prepend?: React.ReactNode | string; showTimeComparison?: boolean; - showCorrelations?: boolean; + showTransactionTypeSelector?: boolean; } function getRowDirection(showColumn: boolean) { @@ -85,7 +86,7 @@ function DebugQueryCallout() { export function SearchBar({ prepend, showTimeComparison = false, - showCorrelations = false, + showTransactionTypeSelector = false, }: Props) { const { isMedium, isLarge } = useBreakPoints(); const itemsStyle = { marginBottom: isLarge ? px(unit) : 0 }; @@ -94,8 +95,13 @@ export function SearchBar({ <> + {showTransactionTypeSelector && ( + + + + )} - + + checkRunningSessions$(deps, config).toPromise(); describe('getSearchStatus', () => { let mockClient: any; @@ -32,6 +42,7 @@ describe('getSearchStatus', () => { maxUpdateRetries: 3, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + monitoringTaskTimeout: moment.duration(5, 'm'), management: {} as any, }; const mockLogger: any = { @@ -41,11 +52,13 @@ describe('getSearchStatus', () => { }; const emptySO = { - persisted: false, - status: SearchSessionStatus.IN_PROGRESS, - created: moment().subtract(moment.duration(3, 'm')), - touched: moment().subtract(moment.duration(10, 's')), - idMapping: {}, + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(10, 's')), + idMapping: {}, + }, }; beforeEach(() => { @@ -171,6 +184,118 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); }); + + test('fetching is abortable', async () => { + let i = 0; + const abort$ = new Subject(); + savedObjectsClient.find.mockImplementation(() => { + return new Promise((resolve) => { + if (++i === 2) { + abort$.next(); + } + resolve({ + saved_objects: i <= 5 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], + total: 25, + page: i, + } as any); + }); + }); + + await checkRunningSessions$( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ) + .pipe(takeUntil(abort$)) + .toPromise(); + + jest.runAllTimers(); + + // if not for `abort$` then this would be called 6 times! + expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); + }); + + test('sorting is by "touched"', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [], + total: 0, + } as any); + + await checkRunningSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ sortField: 'touched', sortOrder: 'asc' }) + ); + }); + + test('sessions fetched in the beginning are processed even if sessions in the end fail', async () => { + let i = 0; + savedObjectsClient.find.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (++i === 2) { + reject(new Error('Fake find error...')); + } + resolve({ + saved_objects: + i <= 5 + ? [ + i === 1 + ? { + id: '123', + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(2, 'm')), + idMapping: { + 'map-key': { + strategy: ENHANCED_ES_SEARCH_STRATEGY, + id: 'async-id', + }, + }, + }, + } + : emptySO, + emptySO, + emptySO, + emptySO, + emptySO, + ] + : [], + total: 25, + page: i, + } as any); + }); + }); + + await checkRunningSessions$( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ).toPromise(); + + jest.runAllTimers(); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); + + // by checking that delete was called we validate that sessions from session that were successfully fetched were processed + expect(mockClient.asyncSearch.delete).toBeCalled(); + const { id } = mockClient.asyncSearch.delete.mock.calls[0][0]; + expect(id).toBe('async-id'); + }); }); describe('delete', () => { diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index 60c7283320d0c..bb1e9643cd0d5 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -13,8 +13,8 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; import moment from 'moment'; -import { EMPTY, from } from 'rxjs'; -import { expand, concatMap } from 'rxjs/operators'; +import { EMPTY, from, Observable } from 'rxjs'; +import { catchError, concatMap } from 'rxjs/operators'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { ENHANCED_ES_SEARCH_STRATEGY, @@ -120,6 +120,9 @@ function getSavedSearchSessionsPage$( perPage: config.pageSize, type: SEARCH_SESSION_TYPE, namespaces: ['*'], + // process older sessions first + sortField: 'touched', + sortOrder: 'asc', filter: nodeBuilder.or([ nodeBuilder.and([ nodeBuilder.is( @@ -134,113 +137,121 @@ function getSavedSearchSessionsPage$( ); } -function getAllSavedSearchSessions$(deps: CheckRunningSessionsDeps, config: SearchSessionsConfig) { - return getSavedSearchSessionsPage$(deps, config, 1).pipe( - expand((result) => { - if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) - return EMPTY; - else { - return getSavedSearchSessionsPage$(deps, config, result.page + 1); - } - }) - ); -} - -export async function checkRunningSessions( +function checkRunningSessionsPage( deps: CheckRunningSessionsDeps, - config: SearchSessionsConfig -): Promise { + config: SearchSessionsConfig, + page: number +) { const { logger, client, savedObjectsClient } = deps; - try { - await getAllSavedSearchSessions$(deps, config) - .pipe( - concatMap(async (runningSearchSessionsResponse) => { - if (!runningSearchSessionsResponse.total) return; - - logger.debug(`Found ${runningSearchSessionsResponse.total} running sessions`); - - const updatedSessions = new Array< - SavedObjectsFindResult - >(); - - await Promise.all( - runningSearchSessionsResponse.saved_objects.map(async (session) => { - const updated = await updateSessionStatus(session, client, logger); - let deleted = false; - - if (!session.attributes.persisted) { - if (isSessionStale(session, config, logger)) { - // delete saved object to free up memory - // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! - // Maybe we want to change state to deleted and cleanup later? - logger.debug(`Deleting stale session | ${session.id}`); + return getSavedSearchSessionsPage$(deps, config, page).pipe( + concatMap(async (runningSearchSessionsResponse) => { + if (!runningSearchSessionsResponse.total) return; + + logger.debug( + `Found ${runningSearchSessionsResponse.total} running sessions, processing ${runningSearchSessionsResponse.saved_objects.length} sessions from page ${page}` + ); + + const updatedSessions = new Array< + SavedObjectsFindResult + >(); + + await Promise.all( + runningSearchSessionsResponse.saved_objects.map(async (session) => { + const updated = await updateSessionStatus(session, client, logger); + let deleted = false; + + if (!session.attributes.persisted) { + if (isSessionStale(session, config, logger)) { + // delete saved object to free up memory + // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! + // Maybe we want to change state to deleted and cleanup later? + logger.debug(`Deleting stale session | ${session.id}`); + try { + await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { + namespace: session.namespaces?.[0], + }); + deleted = true; + } catch (e) { + logger.error( + `Error while deleting stale search session ${session.id}: ${e.message}` + ); + } + + // Send a delete request for each async search to ES + Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { + const searchInfo = session.attributes.idMapping[searchKey]; + if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) { try { - await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { - namespace: session.namespaces?.[0], - }); - deleted = true; + await client.asyncSearch.delete({ id: searchInfo.id }); } catch (e) { logger.error( - `Error while deleting stale search session ${session.id}: ${e.message}` + `Error while deleting async_search ${searchInfo.id}: ${e.message}` ); } - - // Send a delete request for each async search to ES - Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { - const searchInfo = session.attributes.idMapping[searchKey]; - if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) { - try { - await client.asyncSearch.delete({ id: searchInfo.id }); - } catch (e) { - logger.error( - `Error while deleting async_search ${searchInfo.id}: ${e.message}` - ); - } - } - }); } - } + }); + } + } - if (updated && !deleted) { - updatedSessions.push(session); - } - }) - ); - - // Do a bulk update - if (updatedSessions.length) { - // If there's an error, we'll try again in the next iteration, so there's no need to check the output. - const updatedResponse = await savedObjectsClient.bulkUpdate( - updatedSessions.map((session) => ({ - ...session, - namespace: session.namespaces?.[0], - })) - ); + if (updated && !deleted) { + updatedSessions.push(session); + } + }) + ); - const success: Array< - SavedObjectsUpdateResponse - > = []; - const fail: Array> = []; + // Do a bulk update + if (updatedSessions.length) { + // If there's an error, we'll try again in the next iteration, so there's no need to check the output. + const updatedResponse = await savedObjectsClient.bulkUpdate( + updatedSessions.map((session) => ({ + ...session, + namespace: session.namespaces?.[0], + })) + ); - updatedResponse.saved_objects.forEach((savedObjectResponse) => { - if ('error' in savedObjectResponse) { - fail.push(savedObjectResponse); - logger.error( - `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` - ); - } else { - success.push(savedObjectResponse); - } - }); + const success: Array> = []; + const fail: Array> = []; - logger.debug( - `Updating search sessions: success: ${success.length}, fail: ${fail.length}` + updatedResponse.saved_objects.forEach((savedObjectResponse) => { + if ('error' in savedObjectResponse) { + fail.push(savedObjectResponse); + logger.error( + `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` ); + } else { + success.push(savedObjectResponse); } - }) - ) - .toPromise(); - } catch (err) { - logger.error(err); - } + }); + + logger.debug(`Updating search sessions: success: ${success.length}, fail: ${fail.length}`); + } + + return runningSearchSessionsResponse; + }) + ); +} + +export function checkRunningSessions(deps: CheckRunningSessionsDeps, config: SearchSessionsConfig) { + const { logger } = deps; + + const checkRunningSessionsByPage = (nextPage = 1): Observable => + checkRunningSessionsPage(deps, config, nextPage).pipe( + concatMap((result) => { + if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) { + return EMPTY; + } else { + // TODO: while processing previous page session list might have been changed and we might skip a session, + // because it would appear now on a different "page". + // This isn't critical, as we would pick it up on a next task iteration, but maybe we could improve this somehow + return checkRunningSessionsByPage(result.page + 1); + } + }) + ); + + return checkRunningSessionsByPage().pipe( + catchError((e) => { + logger.error(`Error while processing search sessions: ${e?.message}`); + return EMPTY; + }) + ); } diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 101ccb14edf67..c0dc69dfc307b 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -6,10 +6,13 @@ */ import { Duration } from 'moment'; +import { filter, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject } from 'rxjs'; import { TaskManagerSetupContract, TaskManagerStartContract, RunContext, + TaskRunCreatorFunction, } from '../../../../task_manager/server'; import { checkRunningSessions } from './check_running_sessions'; import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; @@ -29,8 +32,9 @@ interface SearchSessionTaskDeps { function searchSessionRunner( core: CoreSetup, { logger, config }: SearchSessionTaskDeps -) { +): TaskRunCreatorFunction { return ({ taskInstance }: RunContext) => { + const aborted$ = new BehaviorSubject(false); return { async run() { const sessionConfig = config.search.sessions; @@ -39,6 +43,8 @@ function searchSessionRunner( logger.debug('Search sessions are disabled. Skipping task.'); return; } + if (aborted$.getValue()) return; + const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); await checkRunningSessions( @@ -48,12 +54,17 @@ function searchSessionRunner( logger, }, sessionConfig - ); + ) + .pipe(takeUntil(aborted$.pipe(filter((aborted) => aborted)))) + .toPromise(); return { state: {}, }; }, + cancel: async () => { + aborted$.next(true); + }, }; }; } @@ -66,6 +77,7 @@ export function registerSearchSessionsTask( [SEARCH_SESSIONS_TASK_TYPE]: { title: 'Search Sessions Monitor', createTaskRunner: searchSessionRunner(core, deps), + timeout: `${deps.config.search.sessions.monitoringTaskTimeout.asSeconds()}s`, }, }); } diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 9344ab973c636..f1f8805a28884 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -75,6 +75,7 @@ describe('SearchSessionService', () => { notTouchedTimeout: moment.duration(2, 'm'), maxUpdateRetries: MAX_UPDATE_RETRIES, defaultExpiration: moment.duration(7, 'd'), + monitoringTaskTimeout: moment.duration(5, 'm'), trackingInterval: moment.duration(10, 's'), management: {} as any, }, @@ -153,6 +154,7 @@ describe('SearchSessionService', () => { maxUpdateRetries: MAX_UPDATE_RETRIES, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + monitoringTaskTimeout: moment.duration(5, 'm'), management: {} as any, }, }, diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index 96224644a457f..4b27b31ad3e0e 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -18,7 +18,6 @@ import { defaultEmbeddableFactoryProvider, EmbeddableContext, PANEL_NOTIFICATION_TRIGGER, - ViewMode, } from '../../../../src/plugins/embeddable/public'; import { EnhancedEmbeddable } from './types'; import { @@ -119,7 +118,6 @@ export class EmbeddableEnhancedPlugin const dynamicActions = new DynamicActionManager({ isCompatible: async (context: unknown) => { if (!this.isEmbeddableContext(context)) return false; - if (context.embeddable.getInput().viewMode !== ViewMode.VIEW) return false; return context.embeddable.runtimeId === embeddable.runtimeId; }, storage, diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index 0caea251ec6fb..0b067e25e32e8 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -38,7 +38,7 @@ yarn test:jest yarn test:jest --watch ``` -Unfortunately coverage collection does not work as automatically, and requires using our handy jest.sh script if you want to run tests on a specific folder and only get coverage numbers for that folder: +Unfortunately coverage collection does not work as automatically, and requires using our handy jest.sh script if you want to run tests on a specific file or folder and only get coverage numbers for that file or folder: ```bash # Running the jest.sh script from the `x-pack/plugins/enterprise_search` folder (vs. kibana root) @@ -46,6 +46,8 @@ Unfortunately coverage collection does not work as automatically, and requires u sh jest.sh {YOUR_COMPONENT_DIR} sh jest.sh public/applications/shared/kibana sh jest.sh server/routes/app_search +# When testing an individual file, remember to pass the path of the test file, not the source file. +sh jest.sh public/applications/shared/flash_messages/flash_messages_logic.test.ts ``` ### E2E tests diff --git a/x-pack/plugins/enterprise_search/jest.sh b/x-pack/plugins/enterprise_search/jest.sh index d7aa0b07fb89c..8bc3134a62d8e 100644 --- a/x-pack/plugins/enterprise_search/jest.sh +++ b/x-pack/plugins/enterprise_search/jest.sh @@ -1,13 +1,21 @@ #! /bin/bash # Whether to run Jest on the entire enterprise_search plugin or a specific component/folder -FOLDER="${1:-all}" -if [[ $FOLDER && $FOLDER != "all" ]] + +TARGET="${1:-all}" +if [[ $TARGET && $TARGET != "all" ]] then - FOLDER=${FOLDER%/} # Strip any trailing slash - FOLDER="${FOLDER}/ --collectCoverageFrom='/x-pack/plugins/enterprise_search/${FOLDER}/**/*.{ts,tsx}'" + # If this is a file + if [[ "$TARGET" == *".ts"* ]]; then + PATH_WITHOUT_EXTENSION=${1%%.*} + TARGET="${TARGET} --collectCoverageFrom='/x-pack/plugins/enterprise_search/${PATH_WITHOUT_EXTENSION}.{ts,tsx}'" + # If this is a folder + else + TARGET=${TARGET%/} # Strip any trailing slash + TARGET="${TARGET}/ --collectCoverageFrom='/x-pack/plugins/enterprise_search/${TARGET}/**/*.{ts,tsx}'" + fi else - FOLDER='' + TARGET='' fi # Pass all remaining arguments (e.g., ...rest) from the 2nd arg onwards @@ -15,4 +23,4 @@ fi # @see https://jestjs.io/docs/en/cli#options ARGS="${*:2}" -yarn test:jest $FOLDER $ARGS +yarn test:jest $TARGET $ARGS diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 88a24755070ec..818245bd50978 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -37,7 +37,6 @@ import { AnalyticsRouter } from '../analytics'; import { ApiLogs } from '../api_logs'; import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; -import { OVERVIEW_TITLE } from '../engine_overview'; import { EngineOverview } from '../engine_overview'; import { ENGINES_TITLE } from '../engines'; import { RelevanceTuning } from '../relevance_tuning'; @@ -122,7 +121,7 @@ export const EngineRouter: React.FC = () => { )} - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index 56fe3b97274ea..6911015e39d4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -12,7 +12,6 @@ import { useValues, useActions } from 'kea'; import { EuiPageContent, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../../shared/telemetry'; import { AppLogic } from '../../../app_logic'; @@ -32,7 +31,6 @@ export const EmptyState: React.FC = () => { return ( <> - {canManageEngines ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx index 3ffe2f3d43a77..8cb26713cb840 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx @@ -12,10 +12,13 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { EuiPageHeader } from '@elastic/eui'; + import { EnginesOverviewHeader } from './'; describe('EnginesOverviewHeader', () => { const wrapper = shallow() + .find(EuiPageHeader) .dive() .children() .dive(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx index df87f2e5230db..bab67fd0e4bb5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx @@ -13,36 +13,42 @@ import { EuiPageHeader, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; +import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { TelemetryLogic } from '../../../../shared/telemetry'; +import { ENGINES_TITLE } from '../constants'; + export const EnginesOverviewHeader: React.FC = () => { const { sendAppSearchTelemetry } = useActions(TelemetryLogic); return ( - - sendAppSearchTelemetry({ - action: 'clicked', - metric: 'header_launch_button', - }) - } - data-test-subj="launchButton" - > - {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', { - defaultMessage: 'Launch App Search', - })} - , - ]} - /> + <> + + + sendAppSearchTelemetry({ + action: 'clicked', + metric: 'header_launch_button', + }) + } + data-test-subj="launchButton" + > + {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', { + defaultMessage: 'Launch App Search', + })} + , + ]} + /> + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx index 56be0a5562742..875c47378d1fb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx @@ -9,14 +9,11 @@ import React from 'react'; import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; - import { EnginesOverviewHeader } from './header'; export const LoadingState: React.FC = () => { return ( <> - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 0712b990159a4..d7e2309fd2a07 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -20,7 +20,6 @@ import { } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LicensingLogic } from '../../../shared/licensing'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination'; @@ -80,7 +79,6 @@ export const EnginesOverview: React.FC = () => { return ( <> - @@ -136,8 +134,9 @@ export const EnginesOverview: React.FC = () => { {canManageEngines && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx index edd417cc1ffe8..9ed6e17c2bcd9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx @@ -9,7 +9,7 @@ import { setMockActions, setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { EuiPageHeader } from '@elastic/eui'; @@ -33,9 +33,11 @@ describe('RelevanceTuningLayout', () => { }); const subject = () => shallow(); + const findButtons = (wrapper: ShallowWrapper) => + wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; it('renders a Save button that will save the current changes', () => { - const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + const buttons = findButtons(subject()); expect(buttons.length).toBe(2); const saveButton = shallow(buttons[0]); saveButton.simulate('click'); @@ -43,7 +45,7 @@ describe('RelevanceTuningLayout', () => { }); it('renders a Reset button that will remove all weights and boosts', () => { - const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + const buttons = findButtons(subject()); expect(buttons.length).toBe(2); const resetButton = shallow(buttons[1]); resetButton.simulate('click'); @@ -55,7 +57,7 @@ describe('RelevanceTuningLayout', () => { ...values, engineHasSchemaFields: false, }); - const buttons = subject().find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + const buttons = findButtons(subject()); expect(buttons.length).toBe(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx index 0ea38b0d9fa36..f29cc12f20a98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx @@ -37,7 +37,7 @@ export const RelevanceTuningLayout: React.FC = ({ engineBreadcrumb, child description={i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description', { - defaultMessage: 'Set field weights and boosts', + defaultMessage: 'Set field weights and boosts.', } )} rightSideItems={ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index ca9b0a886fdd1..4ec38d314a259 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -586,10 +586,9 @@ describe('RelevanceTuningLogic', () => { confirmSpy.mockImplementation(() => false); RelevanceTuningLogic.actions.resetSearchSettings(); + await nextTick(); - expect(http.post).not.toHaveBeenCalledWith( - '/api/app_search/engines/test-engine/search_settings/reset' - ); + expect(http.post).not.toHaveBeenCalled(); }); it('handles errors', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss index 3132894ddc7a1..93bace1d77775 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss @@ -1,10 +1,10 @@ .appSearchResult { display: grid; - grid-template-columns: auto 1fr auto; - grid-template-rows: auto 1fr auto; + grid-template-columns: auto 1fr; + grid-template-rows: auto 1fr; grid-template-areas: - 'drag content actions' - 'drag toggle actions'; + 'drag content' + 'drag toggle'; overflow: hidden; // Prevents child background-colors from clipping outside of panel border-radius border: $euiBorderThin; // TODO: Remove after EUI version is bumped beyond 31.8.0 @@ -35,29 +35,6 @@ } } - &__actionButtons { - grid-area: actions; - display: flex; - flex-wrap: no-wrap; - } - - &__actionButton { - display: flex; - justify-content: center; - align-items: center; - width: $euiSize * 2; - border-left: $euiBorderThin; - - &:first-child { - border-left: none; - } - - &:hover, - &:focus { - background-color: $euiPageBackgroundColor; - } - } - &__dragHandle { grid-area: drag; display: flex; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index 3e83717bf9355..ba9944744e5c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -5,12 +5,14 @@ * 2.0. */ +import { mockKibanaValues } from '../../../__mocks__'; + import React from 'react'; import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiButtonIcon, EuiPanel, EuiButtonIconColor } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import { SchemaTypes } from '../../../shared/types'; @@ -63,18 +65,28 @@ describe('Result', () => { ]); }); - it('renders a header', () => { - const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - expect(header.exists()).toBe(true); - expect(header.prop('isMetaEngine')).toBe(true); // passed through from props - expect(header.prop('showScore')).toBe(true); // passed through from props - expect(header.prop('shouldLinkToDetailPage')).toBe(false); // passed through from props - expect(header.prop('resultMeta')).toEqual({ - id: '1', - score: 100, - engine: 'my-engine', - }); // passed through from meta in result prop + describe('header', () => { + it('renders a header', () => { + const wrapper = shallow(); + const header = wrapper.find(ResultHeader); + + expect(header.exists()).toBe(true); + expect(header.prop('isMetaEngine')).toBe(true); // passed through from props + expect(header.prop('showScore')).toBe(true); // passed through from props + expect(header.prop('resultMeta')).toEqual({ + id: '1', + score: 100, + engine: 'my-engine', + }); // passed through from meta in result prop + expect(header.prop('documentLink')).toBe(undefined); // based on shouldLinkToDetailPage prop + }); + + it('passes documentLink when shouldLinkToDetailPage is true', () => { + const wrapper = shallow(); + const header = wrapper.find(ResultHeader); + + expect(header.prop('documentLink')).toBe('/engines/my-engine/documents/1'); + }); }); describe('actions', () => { @@ -83,53 +95,30 @@ describe('Result', () => { title: 'Hide', onClick: jest.fn(), iconType: 'eyeClosed', - iconColor: 'danger' as EuiButtonIconColor, }, { title: 'Bookmark', onClick: jest.fn(), iconType: 'starFilled', - iconColor: undefined, }, ]; - it('will render an action button in the header for each action passed', () => { + it('passes actions to the header', () => { const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - const renderedActions = shallow(header.prop('actions') as any); - const buttons = renderedActions.find(EuiButtonIcon); - expect(buttons).toHaveLength(2); - - expect(buttons.first().prop('iconType')).toEqual('eyeClosed'); - expect(buttons.first().prop('color')).toEqual('danger'); - buttons.first().simulate('click'); - expect(actions[0].onClick).toHaveBeenCalled(); - - expect(buttons.last().prop('iconType')).toEqual('starFilled'); - // Note that no iconColor was passed so it was defaulted to primary - expect(buttons.last().prop('color')).toEqual('primary'); - buttons.last().simulate('click'); - expect(actions[1].onClick).toHaveBeenCalled(); + expect(wrapper.find(ResultHeader).prop('actions')).toEqual(actions); }); - it('will render a document detail link as the first action if shouldLinkToDetailPage is passed', () => { + it('adds a link action to the start of the actions array if shouldLinkToDetailPage is passed', () => { const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - const renderedActions = shallow(header.prop('actions') as any); - const buttons = renderedActions.find(EuiButtonIcon); - // In addition to the 2 actions passed, we also have a link action - expect(buttons).toHaveLength(3); + const passedActions = wrapper.find(ResultHeader).prop('actions'); + expect(passedActions.length).toEqual(3); // In addition to the 2 actions passed, we also have a link action - expect(buttons.first().prop('data-test-subj')).toEqual('DocumentDetailLink'); - }); + const linkAction = passedActions[0]; + expect(linkAction.title).toEqual('Visit document details'); - it('will not render anything if no actions are passed and shouldLinkToDetailPage is false', () => { - const wrapper = shallow(); - const header = wrapper.find(ResultHeader); - const renderedActions = shallow(header.prop('actions') as any); - const buttons = renderedActions.find(EuiButtonIcon); - expect(buttons).toHaveLength(0); + linkAction.onClick(); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/engines/my-engine/documents/1'); }); }); @@ -148,9 +137,7 @@ describe('Result', () => { }); it('will render field details with type highlights if schemaForTypeHighlights has been provided', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find(ResultField).map((rf) => rf.prop('type'))).toEqual([ 'text', 'text', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index 71d9f39d802d5..d9c16a877dc59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -10,12 +10,11 @@ import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import './result.scss'; -import { EuiButtonIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiPanel, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; - +import { KibanaLogic } from '../../../shared/kibana'; import { Schema } from '../../../shared/types'; import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; @@ -56,48 +55,27 @@ export const Result: React.FC = ({ [result] ); const numResults = resultFields.length; - const documentLink = generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, { - engineName: resultMeta.engine, - documentId: resultMeta.id, - }); const typeForField = (fieldName: string) => { if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName]; }; - const ResultActions = () => { - if (!shouldLinkToDetailPage && !actions.length) return null; - return ( - - - {shouldLinkToDetailPage && ( - - - - - - )} - {actions.map(({ onClick, title, iconType, iconColor }) => ( - - - - ))} - - - ); - }; + const documentLink = shouldLinkToDetailPage + ? generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, { + engineName: resultMeta.engine, + documentId: resultMeta.id, + }) + : undefined; + if (shouldLinkToDetailPage && documentLink) { + const linkAction = { + onClick: () => KibanaLogic.values.navigateToUrl(documentLink), + title: i18n.translate('xpack.enterpriseSearch.appSearch.result.documentDetailLink', { + defaultMessage: 'Visit document details', + }), + iconType: 'eye', + }; + actions = [linkAction, ...actions]; + } return ( = ({ resultMeta={resultMeta} showScore={!!showScore} isMetaEngine={isMetaEngine} - shouldLinkToDetailPage={shouldLinkToDetailPage} - actions={} + documentLink={documentLink} + actions={actions} /> {resultFields .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.test.tsx new file mode 100644 index 0000000000000..4aae1e07f0f8c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButtonIcon, EuiButtonIconColor } from '@elastic/eui'; + +import { ResultActions } from './result_actions'; + +describe('ResultActions', () => { + const actions = [ + { + title: 'Hide', + onClick: jest.fn(), + iconType: 'eyeClosed', + iconColor: 'danger' as EuiButtonIconColor, + }, + { + title: 'Bookmark', + onClick: jest.fn(), + iconType: 'starFilled', + iconColor: undefined, + }, + ]; + + const wrapper = shallow(); + const buttons = wrapper.find(EuiButtonIcon); + + it('renders an action button for each action passed', () => { + expect(buttons).toHaveLength(2); + }); + + it('passes icon props correctly', () => { + expect(buttons.first().prop('iconType')).toEqual('eyeClosed'); + expect(buttons.first().prop('color')).toEqual('danger'); + + expect(buttons.last().prop('iconType')).toEqual('starFilled'); + // Note that no iconColor was passed so it was defaulted to primary + expect(buttons.last().prop('color')).toEqual('primary'); + }); + + it('passes click events', () => { + buttons.first().simulate('click'); + expect(actions[0].onClick).toHaveBeenCalled(); + + buttons.last().simulate('click'); + expect(actions[1].onClick).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx new file mode 100644 index 0000000000000..52fbee90fe31a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { ResultAction } from './types'; + +interface Props { + actions: ResultAction[]; +} + +export const ResultActions: React.FC = ({ actions }) => { + return ( + + {actions.map(({ onClick, title, iconType, iconColor }) => ( + + + + ))} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss index cd1042998dd34..ebae11ee8ad33 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss @@ -1,26 +1,3 @@ .appSearchResultHeader { - display: flex; - margin-bottom: $euiSizeS; - - @include euiBreakpoint('xs') { - flex-direction: column; - } - - &__column { - display: flex; - flex-wrap: wrap; - - @include euiBreakpoint('xs') { - flex-direction: column; - } - - & + &, - .appSearchResultHeaderItem + .appSearchResultHeaderItem { - margin-left: $euiSizeL; - - @include euiBreakpoint('xs') { - margin-left: 0; - } - } - } + margin-bottom: $euiSizeM; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx index 80cff9b96a3ca..cdd43c3efd97a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { ResultActions } from './result_actions'; import { ResultHeader } from './result_header'; describe('ResultHeader', () => { @@ -17,30 +18,27 @@ describe('ResultHeader', () => { score: 100, engine: 'my-engine', }; + const props = { + showScore: false, + isMetaEngine: false, + resultMeta, + actions: [], + }; it('renders', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBe(false); }); it('always renders an id', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1'); expect(wrapper.find('[data-test-subj="ResultId"]').prop('href')).toBeUndefined(); }); - it('renders id as a link if shouldLinkToDetailPage is true', () => { + it('renders id as a link if a documentLink has been passed', () => { const wrapper = shallow( - + ); expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1'); expect(wrapper.find('[data-test-subj="ResultId"]').prop('href')).toEqual( @@ -50,47 +48,39 @@ describe('ResultHeader', () => { describe('score', () => { it('renders score if showScore is true ', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultScore"]').prop('value')).toEqual(100); }); it('does not render score if showScore is false', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultScore"]').exists()).toBe(false); }); }); describe('engine', () => { it('renders engine name if this is a meta engine', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultEngine"]').prop('value')).toBe('my-engine'); }); it('does not render an engine if this is not a meta engine', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ResultEngine"]').exists()).toBe(false); }); }); + + describe('actions', () => { + const actions = [{ title: 'View document', onClick: () => {}, iconType: 'eye' }]; + + it('renders ResultActions if actions have been passed', () => { + const wrapper = shallow(); + expect(wrapper.find(ResultActions).exists()).toBe(true); + }); + + it('does not render ResultActions if no actions are passed', () => { + const wrapper = shallow(); + expect(wrapper.find(ResultActions).exists()).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx index 93a684b1968a2..f577b481b39cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx @@ -9,11 +9,9 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; -import { generateEncodedPath } from '../../utils/encode_path_params'; - +import { ResultActions } from './result_actions'; import { ResultHeaderItem } from './result_header_item'; -import { ResultMeta } from './types'; +import { ResultMeta, ResultAction } from './types'; import './result_header.scss'; @@ -21,8 +19,8 @@ interface Props { showScore: boolean; isMetaEngine: boolean; resultMeta: ResultMeta; - actions?: React.ReactNode; - shouldLinkToDetailPage?: boolean; + actions: ResultAction[]; + documentLink?: string; } export const ResultHeader: React.FC = ({ @@ -30,19 +28,20 @@ export const ResultHeader: React.FC = ({ resultMeta, isMetaEngine, actions, - shouldLinkToDetailPage = false, + documentLink, }) => { - const documentLink = generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, { - engineName: resultMeta.engine, - documentId: resultMeta.id, - }); - return ( -
- +
+ = ({ /> )} - {actions} + {actions.length > 0 && ( + + + + )}
); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss index df3e2ec241106..94367ae634b7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss @@ -1,12 +1,12 @@ -.euiFlexItem:not(:first-child):not(:last-child) .appSearchResultHeaderItem { - padding-right: .75rem; - box-shadow: inset -1px 0 0 0 $euiBorderColor; -} - .appSearchResultHeaderItem { @include euiCodeFont; &__score { color: $euiColorSuccessText; } + + .euiFlexItem:not(:first-child):not(:last-child) & { + padding-right: $euiSizeS; + box-shadow: inset (-$euiBorderWidthThin) 0 0 0 $euiBorderColor; + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx index e0407b4db7f25..d45eb8856d118 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx @@ -69,7 +69,7 @@ describe('ResultHeaderItem', () => { const wrapper = shallow( ); - expect(wrapper.find('ReactRouterHelper').exists()).toBe(true); - expect(wrapper.find('ReactRouterHelper').prop('to')).toBe('http://www.example.com'); + expect(wrapper.find('EuiLinkTo').exists()).toBe(true); + expect(wrapper.find('EuiLinkTo').prop('to')).toBe('http://www.example.com'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx index 545b85c17a529..cf3b385fd9257 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx @@ -9,7 +9,7 @@ import React from 'react'; import './result_header_item.scss'; -import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; +import { EuiLinkTo } from '../../../shared/react_router_helpers/eui_components'; import { TruncatedContent } from '../../../shared/truncate'; @@ -48,11 +48,9 @@ export const ResultHeaderItem: React.FC = ({ field, type, value, href })   {href ? ( - - - - - + + + ) : ( )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index 9eda1362e04fc..5365cc0f029f8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -11,7 +11,9 @@ import { setMockValues, setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiPageHeader } from '@elastic/eui'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; @@ -24,6 +26,9 @@ describe('RelevanceTuning', () => { const actions = { initializeResultSettingsData: jest.fn(), + saveResultSettings: jest.fn(), + confirmResetAllFields: jest.fn(), + clearAllFields: jest.fn(), }; beforeEach(() => { @@ -32,8 +37,12 @@ describe('RelevanceTuning', () => { jest.clearAllMocks(); }); + const subject = () => shallow(); + const findButtons = (wrapper: ShallowWrapper) => + wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; + it('renders', () => { - const wrapper = shallow(); + const wrapper = subject(); expect(wrapper.find(ResultSettingsTable).exists()).toBe(true); expect(wrapper.find(SampleResponse).exists()).toBe(true); }); @@ -47,8 +56,32 @@ describe('RelevanceTuning', () => { setMockValues({ dataLoading: true, }); - const wrapper = shallow(); + const wrapper = subject(); expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); expect(wrapper.find(SampleResponse).exists()).toBe(false); }); + + it('renders a "save" button that will save the current changes', () => { + const buttons = findButtons(subject()); + expect(buttons.length).toBe(3); + const saveButton = shallow(buttons[0]); + saveButton.simulate('click'); + expect(actions.saveResultSettings).toHaveBeenCalled(); + }); + + it('renders a "restore defaults" button that will reset all values to their defaults', () => { + const buttons = findButtons(subject()); + expect(buttons.length).toBe(3); + const resetButton = shallow(buttons[1]); + resetButton.simulate('click'); + expect(actions.confirmResetAllFields).toHaveBeenCalled(); + }); + + it('renders a "clear" button that will remove all selected options', () => { + const buttons = findButtons(subject()); + expect(buttons.length).toBe(3); + const clearButton = shallow(buttons[2]); + clearButton.simulate('click'); + expect(actions.clearAllFields).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 336f3f663119f..a513d0c1b9f34 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -9,12 +9,15 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiPageHeader, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - import { Loading } from '../../../shared/loading'; +import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; @@ -23,13 +26,23 @@ import { SampleResponse } from './sample_response'; import { ResultSettingsLogic } from '.'; +const CLEAR_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.clearButtonLabel', + { defaultMessage: 'Clear all values' } +); + interface Props { engineBreadcrumb: string[]; } export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { const { dataLoading } = useValues(ResultSettingsLogic); - const { initializeResultSettingsData } = useActions(ResultSettingsLogic); + const { + initializeResultSettingsData, + saveResultSettings, + confirmResetAllFields, + clearAllFields, + } = useActions(ResultSettingsLogic); useEffect(() => { initializeResultSettingsData(); @@ -40,7 +53,33 @@ export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { return ( <> - + + {SAVE_BUTTON_LABEL} + , + + {RESTORE_DEFAULTS_BUTTON_LABEL} + , + + {CLEAR_BUTTON_LABEL} + , + ]} + /> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts index a9c161b2bb5be..8d9c33e3c9e68 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -15,7 +15,7 @@ import { nextTick } from '@kbn/test/jest'; import { Schema, SchemaConflicts, SchemaTypes } from '../../../shared/types'; -import { OpenModal, ServerFieldResultSettingObject } from './types'; +import { ServerFieldResultSettingObject } from './types'; import { ResultSettingsLogic } from '.'; @@ -25,7 +25,6 @@ describe('ResultSettingsLogic', () => { const DEFAULT_VALUES = { dataLoading: true, saving: false, - openModal: OpenModal.None, resultFields: {}, lastSavedResultFields: {}, schema: {}, @@ -83,7 +82,6 @@ describe('ResultSettingsLogic', () => { mount({ dataLoading: true, saving: true, - openModal: OpenModal.ConfirmSaveModal, }); ResultSettingsLogic.actions.initializeResultFields( @@ -139,8 +137,6 @@ describe('ResultSettingsLogic', () => { snippetFallback: false, }, }, - // The modal should be reset back to closed if it had been opened previously - openModal: OpenModal.None, // Stores the provided schema details schema, schemaConflicts, @@ -156,47 +152,6 @@ describe('ResultSettingsLogic', () => { }); }); - describe('openConfirmSaveModal', () => { - mount({ - openModal: OpenModal.None, - }); - - ResultSettingsLogic.actions.openConfirmSaveModal(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.ConfirmSaveModal, - }); - }); - - describe('openConfirmResetModal', () => { - mount({ - openModal: OpenModal.None, - }); - - ResultSettingsLogic.actions.openConfirmResetModal(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.ConfirmResetModal, - }); - }); - - describe('closeModals', () => { - it('should close open modals', () => { - mount({ - openModal: OpenModal.ConfirmSaveModal, - }); - - ResultSettingsLogic.actions.closeModals(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.None, - }); - }); - }); - describe('clearAllFields', () => { it('should remove all settings that have been set for each field', () => { mount({ @@ -237,19 +192,6 @@ describe('ResultSettingsLogic', () => { }, }); }); - - it('should close open modals', () => { - mount({ - openModal: OpenModal.ConfirmSaveModal, - }); - - ResultSettingsLogic.actions.resetAllFields(); - - expect(resultSettingLogicValues()).toEqual({ - ...DEFAULT_VALUES, - openModal: OpenModal.None, - }); - }); }); describe('updateField', () => { @@ -297,7 +239,7 @@ describe('ResultSettingsLogic', () => { }); describe('saving', () => { - it('sets saving to true and close any open modals', () => { + it('sets saving to true', () => { mount({ saving: false, }); @@ -307,7 +249,6 @@ describe('ResultSettingsLogic', () => { expect(resultSettingLogicValues()).toEqual({ ...DEFAULT_VALUES, saving: true, - openModal: OpenModal.None, }); }); }); @@ -563,6 +504,12 @@ describe('ResultSettingsLogic', () => { describe('listeners', () => { const { http } = mockHttpValues; const { flashAPIErrors } = mockFlashMessageHelpers; + let confirmSpy: jest.SpyInstance; + + beforeAll(() => { + confirmSpy = jest.spyOn(window, 'confirm'); + }); + afterAll(() => confirmSpy.mockRestore()); const serverFieldResultSettings = { foo: { @@ -864,20 +811,55 @@ describe('ResultSettingsLogic', () => { }); }); + describe('confirmResetAllFields', () => { + it('will reset all fields as long as the user confirms the action', async () => { + mount(); + confirmSpy.mockImplementation(() => true); + jest.spyOn(ResultSettingsLogic.actions, 'resetAllFields'); + + ResultSettingsLogic.actions.confirmResetAllFields(); + + expect(ResultSettingsLogic.actions.resetAllFields).toHaveBeenCalled(); + }); + + it('will do nothing if the user cancels the action', async () => { + mount(); + confirmSpy.mockImplementation(() => false); + jest.spyOn(ResultSettingsLogic.actions, 'resetAllFields'); + + ResultSettingsLogic.actions.confirmResetAllFields(); + + expect(ResultSettingsLogic.actions.resetAllFields).not.toHaveBeenCalled(); + }); + }); + describe('saveResultSettings', () => { + beforeEach(() => { + confirmSpy.mockImplementation(() => true); + }); + it('should make an API call to update result settings and update state accordingly', async () => { + const resultFields = { + foo: { raw: true, rawSize: 100 }, + }; + + const serverResultFields = { + foo: { raw: { size: 100 } }, + }; + mount({ schema, + resultFields, }); http.put.mockReturnValueOnce( Promise.resolve({ - result_fields: serverFieldResultSettings, + result_fields: serverResultFields, }) ); jest.spyOn(ResultSettingsLogic.actions, 'saving'); jest.spyOn(ResultSettingsLogic.actions, 'initializeResultFields'); - ResultSettingsLogic.actions.saveResultSettings(serverFieldResultSettings); + ResultSettingsLogic.actions.saveResultSettings(); expect(ResultSettingsLogic.actions.saving).toHaveBeenCalled(); @@ -887,12 +869,12 @@ describe('ResultSettingsLogic', () => { '/api/app_search/engines/test-engine/result_settings', { body: JSON.stringify({ - result_fields: serverFieldResultSettings, + result_fields: serverResultFields, }), } ); expect(ResultSettingsLogic.actions.initializeResultFields).toHaveBeenCalledWith( - serverFieldResultSettings, + serverResultFields, schema ); }); @@ -901,11 +883,21 @@ describe('ResultSettingsLogic', () => { mount(); http.put.mockReturnValueOnce(Promise.reject('error')); - ResultSettingsLogic.actions.saveResultSettings(serverFieldResultSettings); + ResultSettingsLogic.actions.saveResultSettings(); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); + + it('does nothing if the user does not confirm', async () => { + mount(); + confirmSpy.mockImplementation(() => false); + + ResultSettingsLogic.actions.saveResultSettings(); + await nextTick(); + + expect(http.put).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index c345ae7e02e8d..f518fc945bfbf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -19,7 +19,6 @@ import { DEFAULT_SNIPPET_SIZE } from './constants'; import { FieldResultSetting, FieldResultSettingObject, - OpenModal, ServerFieldResultSettingObject, } from './types'; @@ -34,9 +33,6 @@ import { } from './utils'; interface ResultSettingsActions { - openConfirmResetModal(): void; - openConfirmSaveModal(): void; - closeModals(): void; initializeResultFields( serverResultFields: ServerFieldResultSettingObject, schema: Schema, @@ -62,15 +58,13 @@ interface ResultSettingsActions { updateRawSizeForField(fieldName: string, size: number): { fieldName: string; size: number }; updateSnippetSizeForField(fieldName: string, size: number): { fieldName: string; size: number }; initializeResultSettingsData(): void; - saveResultSettings( - resultFields: ServerFieldResultSettingObject - ): { resultFields: ServerFieldResultSettingObject }; + confirmResetAllFields(): void; + saveResultSettings(): void; } interface ResultSettingsValues { dataLoading: boolean; saving: boolean; - openModal: OpenModal; resultFields: FieldResultSettingObject; lastSavedResultFields: FieldResultSettingObject; schema: Schema; @@ -86,12 +80,25 @@ interface ResultSettingsValues { queryPerformanceScore: number; } +const SAVE_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.confirmSaveMessage', + { + defaultMessage: + 'The changes will start immediately. Make sure your applications are ready to accept the new search results!', + } +); + +const RESET_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.confirmResetMessage', + { + defaultMessage: + 'This will revert your settings back to the default: all fields set to raw. The default will take over immediately and impact your search results.', + } +); + export const ResultSettingsLogic = kea>({ path: ['enterprise_search', 'app_search', 'result_settings_logic'], actions: () => ({ - openConfirmResetModal: () => true, - openConfirmSaveModal: () => true, - closeModals: () => true, initializeResultFields: (serverResultFields, schema, schemaConflicts) => { const resultFields = convertServerResultFieldsToResultFields(serverResultFields, schema); @@ -113,7 +120,8 @@ export const ResultSettingsLogic = kea ({ fieldName, size }), updateSnippetSizeForField: (fieldName, size) => ({ fieldName, size }), initializeResultSettingsData: () => true, - saveResultSettings: (resultFields) => ({ resultFields }), + confirmResetAllFields: () => true, + saveResultSettings: () => true, }), reducers: () => ({ dataLoading: [ @@ -129,17 +137,6 @@ export const ResultSettingsLogic = kea true, }, ], - openModal: [ - OpenModal.None, - { - initializeResultFields: () => OpenModal.None, - closeModals: () => OpenModal.None, - resetAllFields: () => OpenModal.None, - openConfirmResetModal: () => OpenModal.ConfirmResetModal, - openConfirmSaveModal: () => OpenModal.ConfirmSaveModal, - saving: () => OpenModal.None, - }, - ], resultFields: [ {}, { @@ -308,35 +305,42 @@ export const ResultSettingsLogic = kea { - actions.saving(); + confirmResetAllFields: () => { + if (window.confirm(RESET_CONFIRMATION_MESSAGE)) { + actions.resetAllFields(); + } + }, + saveResultSettings: async () => { + if (window.confirm(SAVE_CONFIRMATION_MESSAGE)) { + actions.saving(); - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - const url = `/api/app_search/engines/${engineName}/result_settings`; + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const url = `/api/app_search/engines/${engineName}/result_settings`; - actions.saving(); + actions.saving(); - let response; - try { - response = await http.put(url, { - body: JSON.stringify({ - result_fields: resultFields, - }), - }); - } catch (e) { - flashAPIErrors(e); - } + let response; + try { + response = await http.put(url, { + body: JSON.stringify({ + result_fields: values.reducedServerResultFields, + }), + }); + } catch (e) { + flashAPIErrors(e); + } - actions.initializeResultFields(response.result_fields, values.schema); - setSuccessMessage( - i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.saveSuccessMessage', - { - defaultMessage: 'Result settings have been saved successfully.', - } - ) - ); + actions.initializeResultFields(response.result_fields, values.schema); + setSuccessMessage( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.saveSuccessMessage', + { + defaultMessage: 'Result settings have been saved successfully.', + } + ) + ); + } }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts index 18843112f46bf..1174f65523d99 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -7,11 +7,6 @@ import { FieldValue } from '../result/types'; -export enum OpenModal { - None, - ConfirmResetModal, - ConfirmSaveModal, -} export interface ServerFieldResultSetting { raw?: | { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss index fbc10b5e8ed0f..fe772000f78f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss @@ -43,16 +43,6 @@ } } - &__outer-box { - border: 1px solid #DBE2EB; - padding-right: 16px; - border-radius: 6px; - overflow: hidden; - background-color: #FFFFFF; - box-shadow: 0 2px 2px -1px rgba(152, 162, 179, .3), - 0 1px 5px -2px rgba(152, 162, 179, .3); - } - &__intro-image { background-color: #22272E; display: flex; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index 8edef425f414c..965d71abd5101 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -13,6 +13,7 @@ import { EuiFlexItem, EuiIcon, EuiLink, + EuiPanel, EuiSpacer, EuiText, EuiTextAlign, @@ -51,116 +52,122 @@ export const ConfigCompleted: React.FC = ({ <> {header} - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.heading', - { - defaultMessage: '{name} Configured', - values: { name }, - } - )} -

-
-
- - - {!accountContextOnly ? ( -

+ + + + + + + + + + +

{i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.orgCanConnect.message', + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.heading', { - defaultMessage: '{name} can now be connected to Workplace Search', + defaultMessage: '{name} Configured', values: { name }, } )} -

- ) : ( - -

+

+
+
+ + + {!accountContextOnly ? ( +

{i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.personalConnectLink.message', + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.orgCanConnect.message', { - defaultMessage: - 'Users can now link their {name} accounts from their personal dashboards.', + defaultMessage: '{name} can now be connected to Workplace Search', values: { name }, } )}

- {!privateSourcesEnabled && ( -

- - enable private source connection - - ), - }} - /> + ) : ( + +

+ {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.personalConnectLink.message', + { + defaultMessage: + 'Users can now link their {name} accounts from their personal dashboards.', + values: { name }, + } + )}

- )} -

- - {CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK} - -

-
- )} - - -
-
-
-
- - - - - {CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON} - - - {!accountContextOnly && ( + {!privateSourcesEnabled && ( +

+ + enable private source connection + + ), + }} + /> +

+ )} +

+ + {CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK} + +

+ + )} + + +
+
+ +
+ + - - {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.connect.button', - { - defaultMessage: 'Connect {name}', - values: { name }, - } - )} - + {CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON} + - )} - + {!accountContextOnly && ( + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.connect.button', + { + defaultMessage: 'Connect {name}', + values: { name }, + } + )} + + + )} + + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx index 8a1cdf0b84274..23bd34cfeb944 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiPanel, EuiSpacer, EuiText, EuiTitle, @@ -52,105 +53,115 @@ export const ConfigurationIntro: React.FC = ({ direction="row" responsive={false} > - - - -
- {CONFIG_INTRO_ALT_TEXT} -
-
- - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.steps.title', - { - defaultMessage: 'How to add {name}', - values: { name }, - } - )} -

-
- - -

{CONFIG_INTRO_STEPS_TEXT}

-
- -
- - - -
- -

{CONFIG_INTRO_STEP1_HEADING}

-
-
-
- - -

- One-Time Action, - }} - /> -

-

{CONFIG_INTRO_STEP1_TEXT}

-
-
-
-
- - - -
- -

{CONFIG_INTRO_STEP2_HEADING}

+ + + + +
+ {CONFIG_INTRO_ALT_TEXT} +
+
+ + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.steps.title', + { + defaultMessage: 'How to add {name}', + values: { name }, + } + )} +

+
+ + +

{CONFIG_INTRO_STEPS_TEXT}

+
+ +
+ + + +
+ +

{CONFIG_INTRO_STEP1_HEADING}

+
+
+
+ + +

+ One-Time Action, + }} + /> +

+

{CONFIG_INTRO_STEP1_TEXT}

-
-
- - -

{CONFIG_INTRO_STEP2_TITLE}

-

{CONFIG_INTRO_STEP2_TEXT}

-
-
-
-
- - - - +
+
+ + - {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.configure.button', - { - defaultMessage: 'Configure {name}', - values: { name }, - } - )} - - - - -
-
- + +
+ +

{CONFIG_INTRO_STEP2_HEADING}

+
+
+
+ + +

{CONFIG_INTRO_STEP2_TITLE}

+

{CONFIG_INTRO_STEP2_TEXT}

+
+
+ + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.configure.button', + { + defaultMessage: 'Configure {name}', + values: { name }, + } + )} + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index a34641784b162..fd45d779e6f2a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -160,7 +160,7 @@ export const ConnectInstance: React.FC = ({ const permissionField = ( <> - +

{CONNECT_DOC_PERMISSIONS_TITLE} @@ -272,12 +272,12 @@ export const ConnectInstance: React.FC = ({ responsive={false} > - - + + {header} - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index 1bf8239a6b399..9689ecfae4a94 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -62,9 +62,10 @@ export const SaveCustom: React.FC = ({ }) => ( <> {header} + - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx index ad16260b1de7c..7a66efe4ba5f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx @@ -187,7 +187,7 @@ export const SourceFeatures: React.FC = ({ features, objTy {includedFeatures.map((featureId, i) => ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index 004f7e5e45bfa..463468d1304b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -88,7 +88,7 @@ describe('SourceRouter', () => { const contentBreadCrumb = wrapper.find(SetPageChrome).at(1); const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2); - expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.OVERVIEW]); + expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs]); expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]); expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index ef9788efbdaf2..b844c86abb919 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -98,7 +98,7 @@ export const SourceRouter: React.FC = () => { - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx index e39d72a861b6f..8d5714fd05792 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx @@ -12,18 +12,14 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiFieldText } from '@elastic/eui'; +import { EuiFieldText, EuiEmptyPrompt } from '@elastic/eui'; import { Loading } from '../../../../shared/loading'; import { ContentSection } from '../../../components/shared/content_section'; import { SourcesTable } from '../../../components/shared/sources_table'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { - GroupOverview, - EMPTY_SOURCES_DESCRIPTION, - EMPTY_USERS_DESCRIPTION, -} from './group_overview'; +import { GroupOverview } from './group_overview'; const deleteGroup = jest.fn(); const showSharedSourcesModal = jest.fn(); @@ -92,7 +88,7 @@ describe('GroupOverview', () => { expect(updateGroupName).toHaveBeenCalled(); }); - it('renders empty state messages', () => { + it('renders empty state', () => { setMockValues({ ...mockValues, group: { @@ -103,10 +99,7 @@ describe('GroupOverview', () => { }); const wrapper = shallow(); - const sourcesSection = wrapper.find('[data-test-subj="GroupContentSourcesSection"]') as any; - const usersSection = wrapper.find('[data-test-subj="GroupUsersSection"]') as any; - expect(sourcesSection.prop('description')).toEqual(EMPTY_SOURCES_DESCRIPTION); - expect(usersSection.prop('description')).toEqual(EMPTY_USERS_DESCRIPTION); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index 375ac7476f9b6..364ca0ba47256 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -12,10 +12,12 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiConfirmModal, + EuiEmptyPrompt, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiPanel, EuiSpacer, EuiHorizontalRule, } from '@elastic/eui'; @@ -24,6 +26,7 @@ import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; import { AppLogic } from '../../../app_logic'; +import noSharedSourcesIcon from '../../../assets/share_circle.svg'; import { ContentSection } from '../../../components/shared/content_section'; import { SourcesTable } from '../../../components/shared/sources_table'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; @@ -145,6 +148,12 @@ export const GroupOverview: React.FC = () => { values: { name }, } ); + const GROUP_SOURCES_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesTitle', + { + defaultMessage: 'Group content sources', + } + ); const GROUP_SOURCES_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesDescription', { @@ -170,15 +179,29 @@ export const GroupOverview: React.FC = () => { const sourcesSection = ( - {hasContentSources && sourcesTable} + {sourcesTable} ); + const sourcesEmptyState = ( + <> + + {GROUP_SOURCES_TITLE}

} + body={

{EMPTY_SOURCES_DESCRIPTION}

} + actions={manageSourcesButton} + /> +
+ + + ); + const usersSection = !isFederatedAuth && ( { <> - {sourcesSection} + {hasContentSources ? sourcesSection : sourcesEmptyState} {usersSection} {nameSection} {canDeleteGroup && deleteSection} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index b2bf0364b2d1f..b82e141bc810e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -60,7 +60,7 @@ export const Groups: React.FC = () => { messages[0].description = ( {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx index 312745ee7496c..68f2a2289c1f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx @@ -152,7 +152,7 @@ export const PrivateSourcesTable: React.FC = ({ {contentSources.map((source, i) => ( {source.name} - + { { messageText={SECURITY_UNSAVED_CHANGES_MESSAGE} /> {header} - {allSourcesToggle} - {!hasPlatinumLicense && platinumLicenseCallout} - {sourceTables} + + {allSourcesToggle} + {!hasPlatinumLicense && platinumLicenseCallout} + {sourceTables} + {confirmModalVisible && confirmModal} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx index 238cba217da8e..a1ac30995f722 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx @@ -144,6 +144,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ isInvalid={Boolean(touchedFields[name] && validation[name])} > updateAgentPolicy({ [name]: e.target.value })} @@ -283,7 +284,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ }} /> - {isEditing && 'id' in agentPolicy ? ( + {isEditing && 'id' in agentPolicy && agentPolicy.is_managed !== true ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index db88de0ba720b..9e23fc775a213 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -167,42 +167,45 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ }), actions: [ { - render: (packagePolicy: InMemoryPackagePolicy) => ( - {}} - // key="packagePolicyView" - // > - // - // , - - - , - // FIXME: implement Copy package policy action - // {}} key="packagePolicyCopy"> - // - // , + render: (packagePolicy: InMemoryPackagePolicy) => { + const menuItems = [ + // FIXME: implement View package policy action + // {}} + // key="packagePolicyView" + // > + // + // , + + + , + // FIXME: implement Copy package policy action + // {}} key="packagePolicyCopy"> + // + // , + ]; + + if (!agentPolicy.is_managed) { + menuItems.push( {(deletePackagePoliciesPrompt) => { return ( @@ -220,10 +223,11 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ ); }} - , - ]} - /> - ), + + ); + } + return ; + }, }, ], }, @@ -244,19 +248,21 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ }} {...rest} search={{ - toolsRight: [ - - - , - ], + toolsRight: agentPolicy.is_managed + ? [] + : [ + + + , + ], box: { incremental: true, schema: true, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx index 350d6439c9d3d..3e6ca5944c380 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx @@ -12,6 +12,8 @@ import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, + EuiIconTip, + EuiTitle, EuiText, EuiSpacer, EuiButtonEmpty, @@ -84,23 +86,42 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { - -

- {isLoading ? ( - - ) : ( - (agentPolicy && agentPolicy.name) || ( - + ) : ( + + + +

+ {(agentPolicy && agentPolicy.name) || ( + + )} +

+
+
+ {agentPolicy?.is_managed && ( + + - ) + )} -

-
+ + )}
{agentPolicy && agentPolicy.description ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index adeb56f489ea3..56b99f645f97c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -194,17 +194,18 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ), }, { - content: ( - - ), + content: + isAgentPolicyLoading || agentPolicyData?.item?.is_managed ? undefined : ( + + ), }, ].map((item, index) => ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 8e9c549fe5609..d01d290e129b8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -341,9 +341,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const isAgentSelectable = (agent: Agent) => { if (!agent.active) return false; + if (!agent.policy_id) return true; - const agentPolicy = agentPolicies.find((p) => p.id === agent.policy_id); - const isManaged = agent.policy_id && agentPolicy?.is_managed === true; + const agentPolicy = agentPoliciesIndexedById[agent.policy_id]; + const isManaged = agentPolicy?.is_managed === true; return !isManaged; }; diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index c85dc06c38286..0959a9a88704a 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -67,10 +67,9 @@ export const postEnrollmentApiKeyHandler: RequestHandler< export const deleteEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { - const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; try { - await APIKeyService.deleteEnrollmentApiKey(soClient, esClient, request.params.keyId); + await APIKeyService.deleteEnrollmentApiKey(esClient, request.params.keyId); const body: DeleteEnrollmentAPIKeyResponse = { action: 'deleted' }; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 5e8abd5966e3a..4427ba714ad6a 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -79,11 +79,12 @@ export const createPackagePolicyHandler: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; + const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; + const { force, ...newPolicy } = request.body; try { const newData = await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', - { ...request.body }, + newPolicy, context, request ); @@ -91,6 +92,7 @@ export const createPackagePolicyHandler: RequestHandler< // Create package policy const packagePolicy = await packagePolicyService.create(soClient, esClient, newData, { user, + force, }); const body: CreatePackagePolicyResponse = { item: packagePolicy }; return response.ok({ @@ -114,7 +116,7 @@ export const updatePackagePolicyHandler: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; + const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; const packagePolicy = await packagePolicyService.get(soClient, request.params.packagePolicyId); if (!packagePolicy) { @@ -155,13 +157,13 @@ export const deletePackagePolicyHandler: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; + const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; try { const body: DeletePackagePoliciesResponse = await packagePolicyService.delete( soClient, esClient, request.body.packagePolicyIds, - { user } + { user, force: request.body.force } ); return response.ok({ body, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index be61a70154b11..7f793a41ab985 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -466,7 +466,9 @@ class AgentPolicyService { esClient: ElasticsearchClient, id: string, packagePolicyIds: string[], - options: { user?: AuthenticatedUser; bumpRevision: boolean } = { bumpRevision: true } + options: { user?: AuthenticatedUser; bumpRevision: boolean; force?: boolean } = { + bumpRevision: true, + } ): Promise { const oldAgentPolicy = await this.get(soClient, id, false); @@ -474,7 +476,7 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } - if (oldAgentPolicy.is_managed) { + if (oldAgentPolicy.is_managed && !options?.force) { throw new IngestManagerError(`Cannot update integrations of managed policy ${id}`); } @@ -497,7 +499,7 @@ class AgentPolicyService { esClient: ElasticsearchClient, id: string, packagePolicyIds: string[], - options?: { user?: AuthenticatedUser } + options?: { user?: AuthenticatedUser; force?: boolean } ): Promise { const oldAgentPolicy = await this.get(soClient, id, false); @@ -505,7 +507,7 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } - if (oldAgentPolicy.is_managed) { + if (oldAgentPolicy.is_managed && !options?.force) { throw new IngestManagerError(`Cannot remove integrations of managed policy ${id}`); } diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts index dc566b2c435a6..3f5f717c94597 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts @@ -56,6 +56,6 @@ export async function agentPolicyUpdateEventHandler( if (action === 'deleted') { await unenrollForAgentPolicyId(soClient, esClient, agentPolicyId); - await deleteEnrollmentApiKeyForAgentPolicyId(soClient, esClient, agentPolicyId); + await deleteEnrollmentApiKeyForAgentPolicyId(esClient, agentPolicyId); } } diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 7059cc96159b9..b8a24a006a674 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -17,7 +17,7 @@ import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { agentPolicyService } from '../agent_policy'; import { escapeSearchQueryPhrase } from '../saved_object'; -import { createAPIKey, invalidateAPIKeys } from './security'; +import { invalidateAPIKeys } from './security'; const uuidRegex = /^\([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\)$/; @@ -77,14 +77,9 @@ export async function getEnrollmentAPIKey( /** * Invalidate an api key and mark it as inactive - * @param soClient * @param id */ -export async function deleteEnrollmentApiKey( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - id: string -) { +export async function deleteEnrollmentApiKey(esClient: ElasticsearchClient, id: string) { const enrollmentApiKey = await getEnrollmentAPIKey(esClient, id); await invalidateAPIKeys([enrollmentApiKey.api_key_id]); @@ -102,7 +97,6 @@ export async function deleteEnrollmentApiKey( } export async function deleteEnrollmentApiKeyForAgentPolicyId( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentPolicyId: string ) { @@ -120,7 +114,7 @@ export async function deleteEnrollmentApiKeyForAgentPolicyId( } for (const apiKey of items) { - await deleteEnrollmentApiKey(soClient, esClient, apiKey.id); + await deleteEnrollmentApiKey(esClient, apiKey.id); } } } @@ -182,19 +176,37 @@ export async function generateEnrollmentAPIKey( } const name = providedKeyName ? `${providedKeyName} (${id})` : id; - const key = await createAPIKey(soClient, name, { - // Useless role to avoid to have the privilege of the user that created the key - 'fleet-apikey-enroll': { - cluster: [], - applications: [ - { - application: '.fleet', - privileges: ['no-privileges'], - resources: ['*'], + + const { body: key } = await esClient.security + .createApiKey({ + body: { + name, + // @ts-expect-error Metadata in api keys + metadata: { + managed_by: 'fleet', + managed: true, + type: 'enroll', + policy_id: data.agentPolicyId, }, - ], - }, - }); + role_descriptors: { + // Useless role to avoid to have the privilege of the user that created the key + 'fleet-apikey-enroll': { + cluster: [], + index: [], + applications: [ + { + application: '.fleet', + privileges: ['no-privileges'], + resources: ['*'], + }, + ], + }, + }, + }, + }) + .catch((err) => { + throw new Error(`Impossible to create an api key: ${err.message}`); + }); if (!key) { throw new Error( diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 418a10225edad..7d12aad6f32b5 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -60,14 +60,14 @@ class PackagePolicyService { soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, packagePolicy: NewPackagePolicy, - options?: { id?: string; user?: AuthenticatedUser; bumpRevision?: boolean } + options?: { id?: string; user?: AuthenticatedUser; bumpRevision?: boolean; force?: boolean } ): Promise { // Check that its agent policy does not have a package policy with the same name const parentAgentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id); if (!parentAgentPolicy) { throw new Error('Agent policy not found'); } - if (parentAgentPolicy.is_managed) { + if (parentAgentPolicy.is_managed && !options?.force) { throw new IngestManagerError( `Cannot add integrations to managed policy ${parentAgentPolicy.id}` ); @@ -77,7 +77,9 @@ class PackagePolicyService { (siblingPackagePolicy) => siblingPackagePolicy.name === packagePolicy.name ) ) { - throw new Error('There is already a package with the same name on this agent policy'); + throw new IngestManagerError( + 'There is already a package with the same name on this agent policy' + ); } // Add ids to stream @@ -106,7 +108,7 @@ class PackagePolicyService { if (isPackageLimited(pkgInfo)) { const agentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id, true); if (agentPolicy && doesAgentPolicyAlreadyIncludePackage(agentPolicy, pkgInfo.name)) { - throw new Error( + throw new IngestManagerError( `Unable to create package policy. Package '${pkgInfo.name}' already exists on this agent policy.` ); } @@ -140,6 +142,7 @@ class PackagePolicyService { { user: options?.user, bumpRevision: options?.bumpRevision ?? true, + force: options?.force, } ); @@ -316,18 +319,14 @@ class PackagePolicyService { const parentAgentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id); if (!parentAgentPolicy) { throw new Error('Agent policy not found'); - } else { - if (parentAgentPolicy.is_managed) { - throw new IngestManagerError(`Cannot update integrations of managed policy ${id}`); - } - if ( - (parentAgentPolicy.package_policies as PackagePolicy[]).find( - (siblingPackagePolicy) => - siblingPackagePolicy.id !== id && siblingPackagePolicy.name === packagePolicy.name - ) - ) { - throw new Error('There is already a package with the same name on this agent policy'); - } + } + if ( + (parentAgentPolicy.package_policies as PackagePolicy[]).find( + (siblingPackagePolicy) => + siblingPackagePolicy.id !== id && siblingPackagePolicy.name === packagePolicy.name + ) + ) { + throw new Error('There is already a package with the same name on this agent policy'); } let inputs = restOfPackagePolicy.inputs.map((input) => @@ -371,7 +370,7 @@ class PackagePolicyService { soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, ids: string[], - options?: { user?: AuthenticatedUser; skipUnassignFromAgentPolicies?: boolean } + options?: { user?: AuthenticatedUser; skipUnassignFromAgentPolicies?: boolean; force?: boolean } ): Promise { const result: DeletePackagePoliciesResponse = []; @@ -389,6 +388,7 @@ class PackagePolicyService { [packagePolicy.id], { user: options?.user, + force: options?.force, } ); } diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 6248b375f8edb..1f39b3135cb3f 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -78,6 +78,7 @@ const PackagePolicyBaseSchema = { export const NewPackagePolicySchema = schema.object({ ...PackagePolicyBaseSchema, + force: schema.maybe(schema.boolean()), }); export const UpdatePackagePolicySchema = schema.object({ diff --git a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts index 3c6f54177096e..6086d1f0e00fb 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts @@ -33,5 +33,6 @@ export const UpdatePackagePolicyRequestSchema = { export const DeletePackagePoliciesRequestSchema = { body: schema.object({ packagePolicyIds: schema.arrayOf(schema.string()), + force: schema.maybe(schema.boolean()), }), }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 4094ecee74e1c..9bd482c73bff5 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -60,6 +60,7 @@ export const datatableVisualization: Visualization groupLabel: i18n.translate('xpack.lens.datatable.groupLabel', { defaultMessage: 'Tabular and single value', }), + sortPriority: 1, }, ], @@ -182,7 +183,7 @@ export const datatableVisualization: Visualization { groupId: 'rows', groupLabel: i18n.translate('xpack.lens.datatable.breakdownRows', { - defaultMessage: 'Split rows', + defaultMessage: 'Rows', }), groupTooltip: i18n.translate('xpack.lens.datatable.breakdownRows.description', { defaultMessage: @@ -209,7 +210,7 @@ export const datatableVisualization: Visualization { groupId: 'columns', groupLabel: i18n.translate('xpack.lens.datatable.breakdownColumns', { - defaultMessage: 'Split columns', + defaultMessage: 'Columns', }), groupTooltip: i18n.translate('xpack.lens.datatable.breakdownColumns.description', { defaultMessage: diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index ef8c0798bb91e..5538dd26d0323 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -219,12 +219,15 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { // reorganize visualizations in groups const grouped: Record< string, - Array< - VisualizationType & { - visualizationId: string; - selection: VisualizationSelection; - } - > + { + priority: number; + visualizations: Array< + VisualizationType & { + visualizationId: string; + selection: VisualizationSelection; + } + >; + } > = {}; // Will need it later on to quickly pick up the metadata from it const lookup: Record< @@ -240,13 +243,17 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { visualizationType.label.toLowerCase().includes(lowercasedSearchTerm) || visualizationType.fullLabel?.toLowerCase().includes(lowercasedSearchTerm); if (isSearchMatch) { - grouped[visualizationType.groupLabel] = grouped[visualizationType.groupLabel] || []; + grouped[visualizationType.groupLabel] = grouped[visualizationType.groupLabel] || { + priority: 0, + visualizations: [], + }; const visualizationEntry = { ...visualizationType, visualizationId, selection: getSelection(visualizationId, visualizationType.id), }; - grouped[visualizationType.groupLabel].push(visualizationEntry); + grouped[visualizationType.groupLabel].priority += visualizationType.sortPriority || 0; + grouped[visualizationType.groupLabel].visualizations.push(visualizationEntry); lookup[`${visualizationId}:${visualizationType.id}`] = visualizationEntry; } } @@ -254,9 +261,11 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { return { visualizationTypes: Object.keys(grouped) - .sort() + .sort((groupA, groupB) => { + return grouped[groupB].priority - grouped[groupA].priority; + }) .flatMap((group): SelectableEntry[] => { - const visualizations = grouped[group]; + const { visualizations } = grouped[group]; if (visualizations.length === 0) { return []; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 8a0b9922c736b..f9058b48dd1a8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -570,7 +570,7 @@ export const InnerVisualizationWrapper = ({ { setLocalState((prevState: WorkspaceState) => ({ diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 9b53e59f96792..fcfed9b9f1fc5 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -21,7 +21,7 @@ export type { YAxisMode, XYCurveType, } from './xy_visualization/types'; -export type { DataType } from './types'; +export type { DataType, OperationMetadata } from './types'; export type { PieVisualizationState, PieLayerState, @@ -33,6 +33,7 @@ export type { IndexPatternPersistedState, PersistedIndexPatternLayer, IndexPatternColumn, + FieldBasedIndexPatternColumn, OperationType, IncompleteColumn, FiltersIndexPatternColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 62cce21ead636..34e2eb2c90122 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -2089,6 +2089,38 @@ describe('state_helpers', () => { }); }); + it('should remove operations indirectly referencing unavailable fields', () => { + const layer: IndexPatternLayer = { + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: '', + dataType: 'number', + operationType: 'moving_average', + isBucketed: false, + scale: 'ratio', + references: ['col2'], + timeScale: undefined, + filter: undefined, + params: { + window: 7, + }, + }, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'average', + sourceField: 'xxx', + }, + }, + indexPatternId: 'original', + }; + const updatedLayer = updateLayerIndexPattern(layer, newIndexPattern); + expect(updatedLayer.columnOrder).toEqual([]); + expect(updatedLayer.columns).toEqual({}); + }); + it('should remove operations referencing fields with insufficient capabilities', () => { const layer: IndexPatternLayer = { columnOrder: ['col1', 'col2'], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 7853b7da7956e..1661e5de8248e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -929,9 +929,17 @@ export function updateLayerIndexPattern( layer: IndexPatternLayer, newIndexPattern: IndexPattern ): IndexPatternLayer { - const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => - isColumnTransferable(column, newIndexPattern) - ); + const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => { + if ('references' in column) { + return ( + isColumnTransferable(column, newIndexPattern) && + column.references.every((columnId) => + isColumnTransferable(layer.columns[columnId], newIndexPattern) + ) + ); + } + return isColumnTransferable(column, newIndexPattern); + }); const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => { const operationDefinition = operationDefinitionMap[column.operationType]; return operationDefinition.transfer diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 79155184a5f6d..18f653c588ee8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -11,6 +11,7 @@ import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/pub import { DragDropIdentifier } from '../drag_drop/providers'; export { + FieldBasedIndexPatternColumn, IndexPatternColumn, OperationType, IncompleteColumn, diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 34b9e4d2b2526..e0977be7535af 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -55,6 +55,7 @@ export const metricVisualization: Visualization = { groupLabel: i18n.translate('xpack.lens.metric.groupLabel', { defaultMessage: 'Tabular and single value', }), + sortPriority: 1, }, ], diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 743846d81213c..c1f885d167659 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -18,7 +18,7 @@ const createStartContract = (): Start => { }), canUseEditor: jest.fn(() => true), navigateToPrefilledEditor: jest.fn(), - getXyVisTypes: jest.fn().mockReturnValue(new Promise(() => visualizationTypes)), + getXyVisTypes: jest.fn().mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))), }; return startContract; }; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 3d34d22c5048a..94b4433a82551 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -550,6 +550,11 @@ export interface VisualizationType { * The group the visualization belongs to */ groupLabel: string; + /** + * The priority of the visualization in the list (global priority) + * Higher number means higher priority. When omitted defaults to 0 + */ + sortPriority?: number; } export interface Visualization { diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 4b371b6dcb930..84b6de1672cd6 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -52,6 +52,7 @@ import { } from './find_exception_list_items'; import { createEndpointList } from './create_endpoint_list'; import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list'; +import { createEndpointEventFiltersList } from './create_endoint_event_filters_list'; export class ExceptionListClient { private readonly user: string; @@ -108,6 +109,18 @@ export class ExceptionListClient { }); }; + /** + * Create the Endpoint Event Filters Agnostic list if it does not yet exist (`null` is returned if it does exist) + */ + public createEndpointEventFiltersList = async (): Promise => { + const { savedObjectsClient, user } = this; + return createEndpointEventFiltersList({ + savedObjectsClient, + user, + version: 1, + }); + }; + /** * This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will * auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form.tsx index bc743fe8d79b4..081272f40b344 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/geo_line_form.tsx @@ -64,7 +64,12 @@ export function GeoLineForm(props: Props) { onChange={onSortFieldChange} fields={props.indexPattern.fields.filter((field) => { const isSplitField = props.splitField ? field.name === props.splitField : false; - return !isSplitField && field.sortable && !indexPatterns.isNestedField(field); + return ( + !isSplitField && + field.sortable && + !indexPatterns.isNestedField(field) && + ['number', 'date'].includes(field.type) + ); })} isClearable={false} /> diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx index 7ffd2a608c43a..1d2354ba3154a 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx @@ -132,17 +132,11 @@ export class ToolsControl extends Component { name: DRAW_BOUNDS_LABEL, panel: 2, }, - ]; - - const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => { - return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; - }); - if (hasGeoPoints) { - tools.push({ + { name: DRAW_DISTANCE_LABEL, panel: 3, - }); - } + }, + ]; return [ { @@ -199,9 +193,7 @@ export class ToolsControl extends Component { { - return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; - })} + geoFields={this.props.geoFields} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} onSubmit={this._initiateDistanceDraw} diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index c15aa8f414fb1..a64a0c0ae09fe 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -10,6 +10,7 @@ export { ChartData } from './types/field_histograms'; export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; export { isPopulatedObject } from './util/object_utils'; -export { isRuntimeMappings } from './util/runtime_field_utils'; export { composeValidators, patternValidator } from './util/validators'; +export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils'; export { extractErrorMessage } from './util/errors'; +export type { RuntimeMappings } from './types/fields'; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts index 77d453b68edc5..5d7f3f934700b 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { estypes } from '@elastic/elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; // import { IndexPatternTitle } from '../kibana'; // import { RuntimeMappings } from '../fields'; // import { JobId } from './job'; @@ -41,17 +41,7 @@ export type ChunkingConfig = estypes.ChunkingConfig; // time_span?: string; // } -export type Aggregation = Record< - string, - { - date_histogram: { - field: string; - fixed_interval: string; - }; - aggregations?: { [key: string]: any }; - aggs?: { [key: string]: any }; - } ->; +export type Aggregation = Record; export type IndicesOptions = estypes.IndicesOptions; // export interface IndicesOptions { diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index 8dfe9d111ed38..45fcfac7e930c 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -28,7 +28,7 @@ export interface Field { aggregatable?: boolean; aggIds?: AggId[]; aggs?: Aggregation[]; - runtimeField?: RuntimeField; + runtimeField?: estypes.RuntimeField; } export interface Aggregation { @@ -108,17 +108,4 @@ export interface AggCardinality { export type RollupFields = Record]>; -// Replace this with import once #88995 is merged -export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; -export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; - -export interface RuntimeField { - type: RuntimeType; - script?: - | string - | { - source: string; - }; -} - export type RuntimeMappings = estypes.RuntimeFields; diff --git a/x-pack/plugins/ml/common/util/datafeed_utils.ts b/x-pack/plugins/ml/common/util/datafeed_utils.ts index c0579ce947992..58038feddb98b 100644 --- a/x-pack/plugins/ml/common/util/datafeed_utils.ts +++ b/x-pack/plugins/ml/common/util/datafeed_utils.ts @@ -18,10 +18,3 @@ export const getDatafeedAggregations = ( ): Aggregation | undefined => { return getAggregations(datafeedConfig); }; - -export const getAggregationBucketsName = (aggregations: any): string | undefined => { - if (aggregations !== null && typeof aggregations === 'object') { - const keys = Object.keys(aggregations); - return keys.length > 0 ? keys[0] : undefined; - } -}; diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 10f5fb975ef5e..da340d4413849 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -8,9 +8,9 @@ import { each, isEmpty, isEqual, pick } from 'lodash'; import semverGte from 'semver/functions/gte'; import moment, { Duration } from 'moment'; +import type { estypes } from '@elastic/elasticsearch'; // @ts-ignore import numeral from '@elastic/numeral'; - import { i18n } from '@kbn/i18n'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; import { parseInterval } from './parse_interval'; @@ -22,13 +22,9 @@ import { MlServerLimits } from '../types/ml_server_info'; import { JobValidationMessage, JobValidationMessageId } from '../constants/messages'; import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_types'; import { MLCATEGORY } from '../constants/field_types'; -import { - getAggregationBucketsName, - getAggregations, - getDatafeedAggregations, -} from './datafeed_utils'; +import { getAggregations, getDatafeedAggregations } from './datafeed_utils'; import { findAggField } from './validation_utils'; -import { isPopulatedObject } from './object_utils'; +import { getFirstKeyInObject, isPopulatedObject } from './object_utils'; import { isDefined } from '../types/guards'; export interface ValidationResults { @@ -52,14 +48,6 @@ export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: numb return freq; } -export function hasRuntimeMappings(job: CombinedJob): boolean { - const hasDatafeed = isPopulatedObject(job.datafeed_config); - if (hasDatafeed) { - return isPopulatedObject(job.datafeed_config.runtime_mappings); - } - return false; -} - export function isTimeSeriesViewJob(job: CombinedJob): boolean { return getSingleMetricViewerJobErrorMessage(job) === undefined; } @@ -85,6 +73,34 @@ export function isMappableJob(job: CombinedJob, detectorIndex: number): boolean return isMappable; } +/** + * Validates that composite definition only have sources that are only terms and date_histogram + * if composite is defined. + * @param buckets + */ +export function hasValidComposite(buckets: estypes.AggregationContainer) { + if ( + isPopulatedObject(buckets, ['composite']) && + isPopulatedObject(buckets.composite, ['sources']) && + Array.isArray(buckets.composite.sources) + ) { + const sources = buckets.composite.sources; + return !sources.some((source) => { + const sourceName = getFirstKeyInObject(source); + if (sourceName !== undefined && isPopulatedObject(source[sourceName])) { + const sourceTypes = Object.keys(source[sourceName]); + return ( + sourceTypes.length === 1 && + sourceTypes[0] !== 'date_histogram' && + sourceTypes[0] !== 'terms' + ); + } + return false; + }); + } + return true; +} + // Returns a flag to indicate whether the source data can be plotted in a time // series chart for the specified detector. export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex: number): boolean { @@ -105,42 +121,42 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex dtr.partition_field_name !== MLCATEGORY && dtr.over_field_name !== MLCATEGORY; - // If the datafeed uses script fields, we can only plot the time series if - // model plot is enabled. Without model plot it will be very difficult or impossible - // to invert to a reverse search of the underlying metric data. - if ( - isSourceDataChartable === true && - job.datafeed_config?.script_fields !== null && - typeof job.datafeed_config?.script_fields === 'object' - ) { + const hasDatafeed = isPopulatedObject(job.datafeed_config); + + if (isSourceDataChartable && hasDatafeed) { // Perform extra check to see if the detector is using a scripted field. - const scriptFields = Object.keys(job.datafeed_config.script_fields); - isSourceDataChartable = - scriptFields.indexOf(dtr.partition_field_name!) === -1 && - scriptFields.indexOf(dtr.by_field_name!) === -1 && - scriptFields.indexOf(dtr.over_field_name!) === -1; - } + if (isPopulatedObject(job.datafeed_config.script_fields)) { + // If the datafeed uses script fields, we can only plot the time series if + // model plot is enabled. Without model plot it will be very difficult or impossible + // to invert to a reverse search of the underlying metric data. + + const scriptFields = Object.keys(job.datafeed_config.script_fields); + return ( + scriptFields.indexOf(dtr.partition_field_name!) === -1 && + scriptFields.indexOf(dtr.by_field_name!) === -1 && + scriptFields.indexOf(dtr.over_field_name!) === -1 + ); + } - const hasDatafeed = isPopulatedObject(job.datafeed_config); - if (hasDatafeed) { // We cannot plot the source data for some specific aggregation configurations const aggs = getDatafeedAggregations(job.datafeed_config); - if (aggs !== undefined) { - const aggBucketsName = getAggregationBucketsName(aggs); + if (isPopulatedObject(aggs)) { + const aggBucketsName = getFirstKeyInObject(aggs); if (aggBucketsName !== undefined) { - // if fieldName is a aggregated field under nested terms using bucket_script - const aggregations = getAggregations<{ [key: string]: any }>(aggs[aggBucketsName]) ?? {}; + // if fieldName is an aggregated field under nested terms using bucket_script + const aggregations = + getAggregations(aggs[aggBucketsName]) ?? {}; const foundField = findAggField(aggregations, dtr.field_name, false); if (foundField?.bucket_script !== undefined) { return false; } + + // composite sources should be terms and date_histogram only for now + return hasValidComposite(aggregations); } } - // We also cannot plot the source data if they datafeed uses any field defined by runtime_mappings - if (hasRuntimeMappings(job)) { - return false; - } + return true; } } @@ -180,11 +196,22 @@ export function isModelPlotChartableForDetector(job: Job, detectorIndex: number) // Returns a reason to indicate why the job configuration is not supported // if the result is undefined, that means the single metric job should be viewable export function getSingleMetricViewerJobErrorMessage(job: CombinedJob): string | undefined { - // if job has runtime mappings with no model plot - if (hasRuntimeMappings(job) && !job.model_plot_config?.enabled) { - return i18n.translate('xpack.ml.timeSeriesJob.jobWithRunTimeMessage', { - defaultMessage: 'the datafeed contains runtime fields and model plot is disabled', - }); + // if job has at least one composite source that is not terms or date_histogram + const aggs = getDatafeedAggregations(job.datafeed_config); + if (isPopulatedObject(aggs)) { + const aggBucketsName = getFirstKeyInObject(aggs); + if (aggBucketsName !== undefined && aggs[aggBucketsName] !== undefined) { + // if fieldName is an aggregated field under nested terms using bucket_script + + if (!hasValidComposite(aggs[aggBucketsName])) { + return i18n.translate( + 'xpack.ml.timeSeriesJob.jobWithUnsupportedCompositeAggregationMessage', + { + defaultMessage: 'Disabled because the datafeed contains unsupported composite sources.', + } + ); + } + } } // only allow jobs with at least one detector whose function corresponds to // an ES aggregation which can be viewed in the single metric view and which @@ -196,7 +223,7 @@ export function getSingleMetricViewerJobErrorMessage(job: CombinedJob): string | if (isChartableTimeSeriesViewJob === false) { return i18n.translate('xpack.ml.timeSeriesJob.notViewableTimeSeriesJobMessage', { - defaultMessage: 'not a viewable time series job', + defaultMessage: 'Disabled because not a viewable time series job.', }); } } diff --git a/x-pack/plugins/ml/common/util/object_utils.test.ts b/x-pack/plugins/ml/common/util/object_utils.test.ts index 8e4196ed4d826..d6d500cdb82c6 100644 --- a/x-pack/plugins/ml/common/util/object_utils.test.ts +++ b/x-pack/plugins/ml/common/util/object_utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isPopulatedObject } from './object_utils'; +import { getFirstKeyInObject, isPopulatedObject } from './object_utils'; describe('object_utils', () => { describe('isPopulatedObject()', () => { @@ -47,4 +47,18 @@ describe('object_utils', () => { ).toBe(false); }); }); + + describe('getFirstKeyInObject()', () => { + it('gets the first key in object', () => { + expect(getFirstKeyInObject({ attribute1: 'value', attribute2: 'value2' })).toBe('attribute1'); + }); + + it('returns undefined with invalid argument', () => { + expect(getFirstKeyInObject(undefined)).toBe(undefined); + expect(getFirstKeyInObject(null)).toBe(undefined); + expect(getFirstKeyInObject({})).toBe(undefined); + expect(getFirstKeyInObject('value')).toBe(undefined); + expect(getFirstKeyInObject(5)).toBe(undefined); + }); + }); }); diff --git a/x-pack/plugins/ml/common/util/object_utils.ts b/x-pack/plugins/ml/common/util/object_utils.ts index 537ee9202b4de..cd62ca006725e 100644 --- a/x-pack/plugins/ml/common/util/object_utils.ts +++ b/x-pack/plugins/ml/common/util/object_utils.ts @@ -34,3 +34,14 @@ export const isPopulatedObject = ( requiredAttributes.every((d) => ({}.hasOwnProperty.call(arg, d)))) ); }; + +/** + * Get the first key in the object + * getFirstKeyInObject({ firstKey: {}, secondKey: {}}) -> firstKey + */ +export const getFirstKeyInObject = (arg: unknown): string | undefined => { + if (isPopulatedObject(arg)) { + const keys = Object.keys(arg); + return keys.length > 0 ? keys[0] : undefined; + } +}; diff --git a/x-pack/plugins/ml/common/util/runtime_field_utils.ts b/x-pack/plugins/ml/common/util/runtime_field_utils.ts index 6d911ecd5d3cb..7be2a3ec8c9e1 100644 --- a/x-pack/plugins/ml/common/util/runtime_field_utils.ts +++ b/x-pack/plugins/ml/common/util/runtime_field_utils.ts @@ -4,14 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { estypes } from '@elastic/elasticsearch'; import { isPopulatedObject } from './object_utils'; import { RUNTIME_FIELD_TYPES } from '../../../../../src/plugins/data/common'; -import type { RuntimeField, RuntimeMappings } from '../types/fields'; +import type { RuntimeMappings } from '../types/fields'; type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; -export function isRuntimeField(arg: unknown): arg is RuntimeField { +export function isRuntimeField(arg: unknown): arg is estypes.RuntimeField { return ( ((isPopulatedObject(arg, ['type']) && Object.keys(arg).length === 1) || (isPopulatedObject(arg, ['type', 'script']) && diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index d3e58c4d7bb0d..f723c1d72b818 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from 'src/core/public'; +import type { estypes } from '@elastic/elasticsearch'; import { IndexPattern, IFieldType, @@ -49,7 +50,7 @@ import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; -import { RuntimeMappings, RuntimeField } from '../../../../common/types/fields'; +import { RuntimeMappings } from '../../../../common/types/fields'; import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; export const INIT_MAX_COLUMNS = 10; @@ -179,7 +180,7 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results export const NON_AGGREGATABLE = 'non-aggregatable'; export const getDataGridSchemaFromESFieldType = ( - fieldType: ES_FIELD_TYPES | undefined | RuntimeField['type'] + fieldType: ES_FIELD_TYPES | undefined | estypes.RuntimeField['type'] ): string | undefined => { // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] // To fall back to the default string schema it needs to be undefined. diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx new file mode 100644 index 0000000000000..10deaa1c2d489 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; + +import { IntlProvider } from 'react-intl'; + +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; + +import { ScatterplotMatrix } from './scatterplot_matrix'; + +const mockEsSearch = jest.fn((body) => ({ + hits: { hits: [{ fields: { x: [1], y: [2] } }, { fields: { x: [2], y: [3] } }] }, +})); +jest.mock('../../contexts/kibana', () => ({ + useMlApiContext: () => ({ + esSearch: mockEsSearch, + }), +})); + +const mockEuiTheme = euiThemeLight; +jest.mock('../color_range_legend', () => ({ + useCurrentEuiTheme: () => ({ + euiTheme: mockEuiTheme, + }), +})); + +// Mocking VegaChart to avoid a jest/canvas related error +jest.mock('../vega_chart', () => ({ + VegaChart: () =>
, +})); + +describe('Data Frame Analytics: ', () => { + it('renders the scatterplot matrix wrapper with options but not the chart itself', async () => { + // prepare + render( + + + + ); + + // assert + await waitFor(() => { + expect(mockEsSearch).toHaveBeenCalledTimes(0); + // should hide the loading indicator and render the wrapping options boilerplate + expect(screen.queryByTestId('mlScatterplotMatrix loaded')).toBeInTheDocument(); + // should not render the scatterplot matrix itself because there's no data items. + expect(screen.queryByTestId('mlVegaChart')).not.toBeInTheDocument(); + }); + }); + + it('renders the scatterplot matrix wrapper with options and the chart itself', async () => { + // prepare + render( + + + + ); + + // assert + await waitFor(() => { + expect(mockEsSearch).toHaveBeenCalledWith({ + body: { _source: false, fields: ['x', 'y'], from: 0, query: undefined, size: 1000 }, + index: 'the-index-name', + }); + // should hide the loading indicator and render the wrapping options boilerplate + expect(screen.queryByTestId('mlScatterplotMatrix loaded')).toBeInTheDocument(); + // should render the scatterplot matrix. + expect(screen.queryByTestId('mlVegaChart')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 540fa65bf6c18..b83965b52befc 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -108,7 +108,7 @@ export const ScatterplotMatrix: FC = ({ // are sized according to outlier_score const [dynamicSize, setDynamicSize] = useState(false); - // used to give the use the option to customize the fields used for the matrix axes + // used to give the user the option to customize the fields used for the matrix axes const [fields, setFields] = useState([]); useEffect(() => { @@ -165,7 +165,7 @@ export const ScatterplotMatrix: FC = ({ useEffect(() => { if (fields.length === 0) { - setSplom(undefined); + setSplom({ columns: [], items: [], messages: [] }); setIsLoading(false); return; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts index 1e1f376049579..79986e8ddb098 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; +import { estypes } from '@elastic/elasticsearch'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { RuntimeType } from '../../../../../../../../../../src/plugins/data/common'; import { EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; @@ -18,7 +18,7 @@ export const CATEGORICAL_TYPES = new Set(['ip', 'keyword']); // Regression supports numeric fields. Classification supports categorical, numeric, and boolean. export const shouldAddAsDepVarOption = ( fieldId: string, - fieldType: ES_FIELD_TYPES | RuntimeType, + fieldType: ES_FIELD_TYPES | estypes.RuntimeField['type'], jobType: AnalyticsJobType ) => { if (fieldId === EVENT_RATE_FIELD_ID) return false; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index f48f4a62f5a7d..2d9ae1cd4689b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -13,7 +13,7 @@ import { CoreSetup } from 'src/core/public'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils'; -import { RuntimeMappings, RuntimeField } from '../../../../../../common/types/fields'; +import { RuntimeMappings } from '../../../../../../common/types/fields'; import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms'; import { DataLoader } from '../../../../datavisualizer/index_based/data_loader'; @@ -44,7 +44,7 @@ interface MLEuiDataGridColumn extends EuiDataGridColumn { function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) { return Object.keys(runtimeMappings).map((id) => { const field = runtimeMappings[id]; - const schema = getDataGridSchemaFromESFieldType(field.type as RuntimeField['type']); + const schema = getDataGridSchemaFromESFieldType(field.type as estypes.RuntimeField['type']); return { id, schema, isExpandable: schema !== 'boolean', isRuntimeFieldColumn: true }; }); } @@ -64,7 +64,7 @@ export const useIndexData = ( const field = indexPattern.fields.getByName(id); const isRuntimeFieldColumn = field?.runtimeField !== undefined; const schema = isRuntimeFieldColumn - ? getDataGridSchemaFromESFieldType(field?.type as RuntimeField['type']) + ? getDataGridSchemaFromESFieldType(field?.type as estypes.RuntimeField['type']) : getDataGridSchemaFromKibanaFieldType(field); return { id, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 60c5a1db9b93b..6c158f103aade 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -33,24 +33,32 @@ import { FeatureImportanceSummaryPanelProps } from '../total_feature_importance_ import { useExplorationUrlState } from '../../hooks/use_exploration_url_state'; import { ExplorationQueryBarProps } from '../exploration_query_bar/exploration_query_bar'; -const filters = { - options: [ - { - id: 'training', - label: i18n.translate('xpack.ml.dataframe.analytics.explorationResults.trainingSubsetLabel', { - defaultMessage: 'Training', - }), - }, - { - id: 'testing', - label: i18n.translate('xpack.ml.dataframe.analytics.explorationResults.testingSubsetLabel', { - defaultMessage: 'Testing', - }), - }, - ], - columnId: 'ml.is_training', - key: { training: true, testing: false }, -}; +function getFilters(resultsField: string) { + return { + options: [ + { + id: 'training', + label: i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.trainingSubsetLabel', + { + defaultMessage: 'Training', + } + ), + }, + { + id: 'testing', + label: i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.testingSubsetLabel', + { + defaultMessage: 'Testing', + } + ), + }, + ], + columnId: `${resultsField}.is_training`, + key: { training: true, testing: false }, + }; +} export interface EvaluatePanelProps { jobConfig: DataFrameAnalyticsConfig; @@ -151,7 +159,7 @@ export const ExplorationPageWrapper: FC = ({ )} - {indexPattern !== undefined && ( + {indexPattern !== undefined && jobConfig && ( <> @@ -162,7 +170,7 @@ export const ExplorationPageWrapper: FC = ({ indexPattern={indexPattern} setSearchQuery={searchQueryUpdateHandler} query={query} - filters={filters} + filters={getFilters(jobConfig.dest.results_field)} /> 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 e09e9f3d2c1ae..1871e8925cb75 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 @@ -10,7 +10,7 @@ import { isEqual } from 'lodash'; import useObservable from 'react-use/lib/useObservable'; import { forkJoin, of, Observable, Subject } from 'rxjs'; -import { mergeMap, switchMap, tap } from 'rxjs/operators'; +import { mergeMap, switchMap, tap, map } from 'rxjs/operators'; import { useCallback, useMemo } from 'react'; import { explorerService } from '../explorer_dashboard_service'; @@ -21,7 +21,6 @@ import { getSelectionTimeRange, loadAnnotationsTableData, loadAnomaliesTableData, - loadDataForCharts, loadFilteredTopInfluencers, loadTopInfluencers, AppStateSelectedCells, @@ -36,8 +35,9 @@ import { ANOMALY_SWIM_LANE_HARD_LIMIT } from '../explorer_constants'; import { TimefilterContract } from '../../../../../../../src/plugins/data/public'; import { AnomalyExplorerChartsService } from '../../services/anomaly_explorer_charts_service'; import { CombinedJob } from '../../../../common/types/anomaly_detection_jobs'; -import { mlJobService } from '../../services/job_service'; import { InfluencersFilterQuery } from '../../../../common/types/es_client'; +import { ExplorerChartsData } from '../explorer_charts/explorer_charts_container_service'; +import { mlJobService } from '../../services/job_service'; // Memoize the data fetching methods. // wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument @@ -58,7 +58,6 @@ const memoize = any>(func: T, context?: any) => { const memoizedLoadAnnotationsTableData = memoize( loadAnnotationsTableData ); -const memoizedLoadDataForCharts = memoize(loadDataForCharts); const memoizedLoadFilteredTopInfluencers = memoize( loadFilteredTopInfluencers ); @@ -96,7 +95,7 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi const loadExplorerDataProvider = ( mlResultsService: MlResultsService, anomalyTimelineService: AnomalyTimelineService, - anomalyExplorerService: AnomalyExplorerChartsService, + anomalyExplorerChartsService: AnomalyExplorerChartsService, timefilter: TimefilterContract ) => { const memoizedLoadOverallData = memoize( @@ -108,8 +107,8 @@ const loadExplorerDataProvider = ( anomalyTimelineService ); const memoizedAnomalyDataChange = memoize( - anomalyExplorerService.getAnomalyData, - anomalyExplorerService + anomalyExplorerChartsService.getAnomalyData, + anomalyExplorerChartsService ); return (config: LoadExplorerDataConfig): Observable> => { @@ -160,9 +159,7 @@ const loadExplorerDataProvider = ( swimlaneBucketInterval.asSeconds(), bounds ), - anomalyChartRecords: memoizedLoadDataForCharts( - lastRefresh, - mlResultsService, + anomalyChartRecords: anomalyExplorerChartsService.loadDataForCharts$( jobIds, timerange.earliestMs, timerange.latestMs, @@ -214,42 +211,30 @@ const loadExplorerDataProvider = ( // 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, topFieldValues }) => { - if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) { - memoizedAnomalyDataChange( - lastRefresh, - explorerService, - combinedJobRecords, - swimlaneContainerWidth, - anomalyChartRecords, - timerange.earliestMs, - timerange.latestMs, - timefilter, - tableSeverity - ); - } else { - memoizedAnomalyDataChange( - lastRefresh, - explorerService, - combinedJobRecords, - swimlaneContainerWidth, - [], - timerange.earliestMs, - timerange.latestMs, - timefilter, - 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. + tap(explorerService.setChartsDataLoading), mergeMap( - ({ anomalyChartRecords, influencers, overallState, topFieldValues }) => + ({ + anomalyChartRecords, + influencers, + overallState, + topFieldValues, + annotationsData, + tableData, + }) => forkJoin({ - influencers: + anomalyChartsData: memoizedAnomalyDataChange( + lastRefresh, + combinedJobRecords, + swimlaneContainerWidth, + selectedCells !== undefined && Array.isArray(anomalyChartRecords) + ? anomalyChartRecords + : [], + timerange.earliestMs, + timerange.latestMs, + timefilter, + tableSeverity + ), + filteredTopInfluencers: (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) && anomalyChartRecords !== undefined && anomalyChartRecords.length > 0 @@ -280,24 +265,26 @@ const loadExplorerDataProvider = ( swimlaneContainerWidth, influencersFilterQuery ), - }), - ( - { annotationsData, overallState, tableData }, - { influencers, viewBySwimlaneState } - ): Partial => { - return { - annotations: annotationsData, - influencers: influencers as any, - loading: false, - viewBySwimlaneDataLoading: false, - overallSwimlaneData: overallState, - viewBySwimlaneData: viewBySwimlaneState as any, - tableData, - swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState) - ? viewBySwimlaneState.cardinality - : undefined, - }; - } + }).pipe( + tap(({ anomalyChartsData }) => { + explorerService.setCharts(anomalyChartsData as ExplorerChartsData); + }), + map(({ viewBySwimlaneState, filteredTopInfluencers }) => { + return { + annotations: annotationsData, + influencers: filteredTopInfluencers as any, + loading: false, + viewBySwimlaneDataLoading: false, + anomalyChartsDataLoading: false, + overallSwimlaneData: overallState, + viewBySwimlaneData: viewBySwimlaneState as any, + tableData, + swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState) + ? viewBySwimlaneState.cardinality + : undefined, + }; + }) + ) ) ); }; @@ -319,7 +306,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any) uiSettings, mlResultsService ); - const anomalyExplorerService = new AnomalyExplorerChartsService( + const anomalyExplorerChartsService = new AnomalyExplorerChartsService( timefilter, mlApiServices, mlResultsService @@ -327,7 +314,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any) return loadExplorerDataProvider( mlResultsService, anomalyTimelineService, - anomalyExplorerService, + anomalyExplorerChartsService, timefilter ); }, []); 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 deleted file mode 100644 index 8fe2c32b766b4..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ /dev/null @@ -1,312 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC, useCallback, useMemo, useState, useEffect } from 'react'; -import { debounce } from 'lodash'; -import { - EuiFormRow, - EuiCheckboxGroup, - EuiInMemoryTableProps, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSpacer, - EuiButtonEmpty, - EuiButton, - EuiModalFooter, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiModalBody } from '@elastic/eui'; -import { EuiInMemoryTable } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../contexts/kibana'; -import { DashboardSavedObject } from '../../../../../../src/plugins/dashboard/public'; -import { getDefaultSwimlanePanelTitle } from '../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; -import { useDashboardService } from '../services/dashboard_service'; -import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; -import { JobId } from '../../../common/types/anomaly_detection_jobs'; -import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../embeddables'; - -export interface DashboardItem { - id: string; - title: string; - description: string | undefined; - attributes: DashboardSavedObject; -} - -export type EuiTableProps = EuiInMemoryTableProps; - -function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { - return { - type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - title: getDefaultSwimlanePanelTitle(jobIds), - }; -} - -interface AddToDashboardControlProps { - jobIds: JobId[]; - viewBy: string; - onClose: (callback?: () => Promise) => void; -} - -/** - * Component for attaching anomaly swim lane embeddable to dashboards. - */ -export const AddToDashboardControl: FC = ({ - onClose, - jobIds, - viewBy, -}) => { - const { - notifications: { toasts }, - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); - - useEffect(() => { - fetchDashboards(); - - return () => { - fetchDashboards.cancel(); - }; - }, []); - - const dashboardService = useDashboardService(); - - const [isLoading, setIsLoading] = useState(false); - const [selectedSwimlanes, setSelectedSwimlanes] = useState<{ [key in SwimlaneType]: boolean }>({ - [SWIMLANE_TYPE.OVERALL]: true, - [SWIMLANE_TYPE.VIEW_BY]: false, - }); - const [dashboardItems, setDashboardItems] = useState([]); - const [selectedItems, setSelectedItems] = useState([]); - - const fetchDashboards = useCallback( - debounce(async (query?: string) => { - try { - const response = await dashboardService.fetchDashboards(query); - const items: DashboardItem[] = response.savedObjects.map((savedObject) => { - return { - id: savedObject.id, - title: savedObject.attributes.title, - description: savedObject.attributes.description, - attributes: savedObject.attributes, - }; - }); - setDashboardItems(items); - } catch (e) { - toasts.danger({ - body: e, - }); - } - setIsLoading(false); - }, 500), - [] - ); - - const search: EuiTableProps['search'] = useMemo(() => { - return { - onChange: ({ queryText }) => { - setIsLoading(true); - fetchDashboards(queryText); - }, - box: { - incremental: true, - 'data-test-subj': 'mlDashboardsSearchBox', - }, - }; - }, []); - - const addSwimlaneToDashboardCallback = useCallback(async () => { - const swimlanes = Object.entries(selectedSwimlanes) - .filter(([, isSelected]) => isSelected) - .map(([swimlaneType]) => swimlaneType); - - for (const selectedDashboard of selectedItems) { - const panelsData = swimlanes.map((swimlaneType) => { - const config = getDefaultEmbeddablePanelConfig(jobIds); - if (swimlaneType === SWIMLANE_TYPE.VIEW_BY) { - return { - ...config, - embeddableConfig: { - jobIds, - swimlaneType, - viewBy, - }, - }; - } - return { - ...config, - embeddableConfig: { - jobIds, - swimlaneType, - }, - }; - }); - - try { - await dashboardService.attachPanels( - selectedDashboard.id, - selectedDashboard.attributes, - panelsData - ); - toasts.success({ - title: ( - - ), - toastLifeTimeMs: 3000, - }); - } catch (e) { - toasts.danger({ - body: e, - }); - } - } - }, [selectedSwimlanes, selectedItems]); - - const columns: EuiTableProps['columns'] = [ - { - field: 'title', - name: i18n.translate('xpack.ml.explorer.dashboardsTable.titleColumnHeader', { - defaultMessage: 'Title', - }), - sortable: true, - truncateText: true, - }, - { - field: 'description', - name: i18n.translate('xpack.ml.explorer.dashboardsTable.descriptionColumnHeader', { - defaultMessage: 'Description', - }), - truncateText: true, - }, - ]; - - const swimlaneTypeOptions = [ - { - id: SWIMLANE_TYPE.OVERALL, - label: i18n.translate('xpack.ml.explorer.overallLabel', { - defaultMessage: 'Overall', - }), - }, - { - id: SWIMLANE_TYPE.VIEW_BY, - label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', { - defaultMessage: 'View by {viewByField}', - values: { viewByField: viewBy }, - }), - }, - ]; - - const selection: EuiTableProps['selection'] = { - onSelectionChange: setSelectedItems, - }; - - const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); - - return ( - - - - - - - - - } - > - { - const newSelection = { - ...selectedSwimlanes, - [optionId]: !selectedSwimlanes[optionId as SwimlaneType], - }; - setSelectedSwimlanes(newSelection); - }} - data-test-subj="mlAddToDashboardSwimlaneTypeSelector" - /> - - - - - - } - data-test-subj="mlDashboardSelectionContainer" - > - - - - - - - - { - onClose(async () => { - const selectedDashboardId = selectedItems[0].id; - await addSwimlaneToDashboardCallback(); - await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); - }); - }} - data-test-subj="mlAddAndEditDashboardButton" - > - - - - - - - - ); -}; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx new file mode 100644 index 0000000000000..9f65449169ee6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, FC } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../contexts/kibana'; +import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils'; +import { TimeRangeBounds } from '../util/time_buckets'; +import { AddAnomalyChartsToDashboardControl } from './dashboard_controls/add_anomaly_charts_to_dashboard_controls'; + +interface AnomalyContextMenuProps { + selectedJobs: ExplorerJob[]; + selectedCells?: AppStateSelectedCells; + bounds?: TimeRangeBounds; + interval?: number; + chartsCount: number; +} +export const AnomalyContextMenu: FC = ({ + selectedJobs, + selectedCells, + bounds, + interval, + chartsCount, +}) => { + const { + services: { + application: { capabilities }, + }, + } = useMlKibana(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isAddDashboardsActive, setIsAddDashboardActive] = useState(false); + + const canEditDashboards = capabilities.dashboard?.createNew ?? false; + const menuItems = useMemo(() => { + const items = []; + if (canEditDashboards) { + items.push( + + + + ); + } + return items; + }, [canEditDashboards]); + + const jobIds = selectedJobs.map(({ id }) => id); + + return ( + <> + {menuItems.length > 0 && ( + + + } + isOpen={isMenuOpen} + closePopover={setIsMenuOpen.bind(null, false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + )} + {isAddDashboardsActive && selectedJobs && ( + { + setIsAddDashboardActive(false); + if (callback) { + await callback(); + } + }} + selectedCells={selectedCells} + bounds={bounds} + interval={interval} + jobIds={jobIds} + /> + )} + + ); +}; 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 7c63d4087ce1e..37967d18dbbd9 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -24,7 +24,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { OVERALL_LABEL, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants'; -import { AddToDashboardControl } from './add_to_dashboard_control'; +import { AddSwimlaneToDashboardControl } from './dashboard_controls/add_swimlane_to_dashboard_controls'; import { useMlKibana } from '../contexts/kibana'; import { TimeBuckets } from '../util/time_buckets'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; @@ -294,7 +294,7 @@ export const AnomalyTimeline: FC = React.memo( )} {isAddDashboardsActive && selectedJobs && ( - { setIsAddDashboardActive(false); if (callback) { diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..5c3c6edee59c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFieldNumber, EuiFormRow, formatDate } from '@elastic/eui'; +import { useDashboardTable } from './use_dashboards_table'; +import { AddToDashboardControl } from './add_to_dashboard_controls'; +import { useAddToDashboardActions } from './use_add_to_dashboard_actions'; +import { AppStateSelectedCells, getSelectionTimeRange } from '../explorer_utils'; +import { TimeRange } from '../../../../../../../src/plugins/data/common/query'; +import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../services/anomaly_explorer_charts_service'; +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../../embeddables'; +import { getDefaultExplorerChartsPanelTitle } from '../../../embeddables/anomaly_charts/anomaly_charts_embeddable'; +import { TimeRangeBounds } from '../../util/time_buckets'; +import { useTableSeverity } from '../../components/controls/select_severity'; +import { MAX_ANOMALY_CHARTS_ALLOWED } from '../../../embeddables/anomaly_charts/anomaly_charts_initializer'; + +function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { + return { + type: ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, + title: getDefaultExplorerChartsPanelTitle(jobIds), + }; +} + +export interface AddToDashboardControlProps { + jobIds: string[]; + selectedCells?: AppStateSelectedCells; + bounds?: TimeRangeBounds; + interval?: number; + onClose: (callback?: () => Promise) => void; +} + +/** + * Component for attaching anomaly swim lane embeddable to dashboards. + */ +export const AddAnomalyChartsToDashboardControl: FC = ({ + onClose, + jobIds, + selectedCells, + bounds, + interval, +}) => { + const [severity] = useTableSeverity(); + const [maxSeriesToPlot, setMaxSeriesToPlot] = useState(DEFAULT_MAX_SERIES_TO_PLOT); + + const getPanelsData = useCallback(async () => { + let timeRange: TimeRange | undefined; + if (selectedCells !== undefined && interval !== undefined && bounds !== undefined) { + const { earliestMs, latestMs } = getSelectionTimeRange(selectedCells, interval, bounds); + timeRange = { + from: formatDate(earliestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'), + to: formatDate(latestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'), + mode: 'absolute', + }; + } + + const config = getDefaultEmbeddablePanelConfig(jobIds); + return [ + { + ...config, + embeddableConfig: { + jobIds, + maxSeriesToPlot: maxSeriesToPlot ?? DEFAULT_MAX_SERIES_TO_PLOT, + severityThreshold: severity.val, + ...(timeRange ?? {}), + }, + }, + ]; + }, [selectedCells, interval, bounds, jobIds, maxSeriesToPlot, severity]); + + const { selectedItems, selection, dashboardItems, isLoading, search } = useDashboardTable(); + const { addToDashboardAndEditCallback, addToDashboardCallback } = useAddToDashboardActions({ + onClose, + getPanelsData, + selectedDashboards: selectedItems, + }); + const title = ( + + ); + + const disabled = selectedItems.length < 1 && !Array.isArray(jobIds === undefined); + + const extraControls = ( + + } + > + setMaxSeriesToPlot(parseInt(e.target.value, 10))} + min={0} + max={MAX_ANOMALY_CHARTS_ALLOWED} + /> + + ); + + return ( + + {extraControls} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..79089e7e5baf9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback, useState } from 'react'; +import { EuiFormRow, EuiCheckboxGroup, EuiInMemoryTableProps, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { DashboardSavedObject } from '../../../../../../../src/plugins/dashboard/public'; +import { getDefaultSwimlanePanelTitle } from '../../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; +import { SWIMLANE_TYPE, SwimlaneType } from '../explorer_constants'; +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../../embeddables'; +import { useDashboardTable } from './use_dashboards_table'; +import { AddToDashboardControl } from './add_to_dashboard_controls'; +import { useAddToDashboardActions } from './use_add_to_dashboard_actions'; + +export interface DashboardItem { + id: string; + title: string; + description: string | undefined; + attributes: DashboardSavedObject; +} + +export type EuiTableProps = EuiInMemoryTableProps; + +function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) { + return { + type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + title: getDefaultSwimlanePanelTitle(jobIds), + }; +} + +interface AddToDashboardControlProps { + jobIds: JobId[]; + viewBy: string; + onClose: (callback?: () => Promise) => void; +} + +/** + * Component for attaching anomaly swim lane embeddable to dashboards. + */ +export const AddSwimlaneToDashboardControl: FC = ({ + onClose, + jobIds, + viewBy, +}) => { + const { selectedItems, selection, dashboardItems, isLoading, search } = useDashboardTable(); + + const [selectedSwimlanes, setSelectedSwimlanes] = useState<{ [key in SwimlaneType]: boolean }>({ + [SWIMLANE_TYPE.OVERALL]: true, + [SWIMLANE_TYPE.VIEW_BY]: false, + }); + + const getPanelsData = useCallback(async () => { + const swimlanes = Object.entries(selectedSwimlanes) + .filter(([, isSelected]) => isSelected) + .map(([swimlaneType]) => swimlaneType); + + return swimlanes.map((swimlaneType) => { + const config = getDefaultEmbeddablePanelConfig(jobIds); + if (swimlaneType === SWIMLANE_TYPE.VIEW_BY) { + return { + ...config, + embeddableConfig: { + jobIds, + swimlaneType, + viewBy, + }, + }; + } + return { + ...config, + embeddableConfig: { + jobIds, + swimlaneType, + }, + }; + }); + }, [selectedSwimlanes, selectedItems]); + const { addToDashboardAndEditCallback, addToDashboardCallback } = useAddToDashboardActions({ + onClose, + getPanelsData, + selectedDashboards: selectedItems, + }); + + const swimlaneTypeOptions = [ + { + id: SWIMLANE_TYPE.OVERALL, + label: i18n.translate('xpack.ml.explorer.overallLabel', { + defaultMessage: 'Overall', + }), + }, + { + id: SWIMLANE_TYPE.VIEW_BY, + label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', { + defaultMessage: 'View by {viewByField}', + values: { viewByField: viewBy }, + }), + }, + ]; + + const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); + + const extraControls = ( + <> + + } + > + { + const newSelection = { + ...selectedSwimlanes, + [optionId]: !selectedSwimlanes[optionId as SwimlaneType], + }; + setSelectedSwimlanes(newSelection); + }} + data-test-subj="mlAddToDashboardSwimlaneTypeSelector" + /> + + + + ); + + const title = ( + + ); + + const disabled = noSwimlaneSelected || selectedItems.length === 0; + return ( + + {extraControls} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx new file mode 100644 index 0000000000000..7806e531834a1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_to_dashboard_controls.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFormRow, + EuiInMemoryTable, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTableProps, useDashboardTable } from './use_dashboards_table'; + +export const columns: EuiTableProps['columns'] = [ + { + field: 'title', + name: i18n.translate('xpack.ml.explorer.dashboardsTable.titleColumnHeader', { + defaultMessage: 'Title', + }), + sortable: true, + truncateText: true, + }, + { + field: 'description', + name: i18n.translate('xpack.ml.explorer.dashboardsTable.descriptionColumnHeader', { + defaultMessage: 'Description', + }), + truncateText: true, + }, +]; + +interface AddToDashboardControlProps extends ReturnType { + onClose: (callback?: () => Promise) => void; + addToDashboardAndEditCallback: () => Promise; + addToDashboardCallback: () => Promise; + title: React.ReactNode; + disabled: boolean; + children?: React.ReactElement; +} +export const AddToDashboardControl: FC = ({ + onClose, + selection, + dashboardItems, + isLoading, + search, + addToDashboardAndEditCallback, + addToDashboardCallback, + title, + disabled, + children, +}) => { + return ( + + + {title} + + + {children} + + } + data-test-subj="mlDashboardSelectionContainer" + > + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx new file mode 100644 index 0000000000000..82c699865f2e4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_add_to_dashboard_actions.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DashboardItem } from './use_dashboards_table'; +import { SavedDashboardPanel } from '../../../../../../../src/plugins/dashboard/common/types'; +import { useMlKibana } from '../../contexts/kibana'; +import { useDashboardService } from '../../services/dashboard_service'; + +export const useAddToDashboardActions = ({ + onClose, + getPanelsData, + selectedDashboards, +}: { + onClose: (callback?: () => Promise) => void; + getPanelsData: ( + selectedDashboards: DashboardItem[] + ) => Promise>>; + selectedDashboards: DashboardItem[]; +}) => { + const { + notifications: { toasts }, + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + const dashboardService = useDashboardService(); + + const addToDashboardCallback = useCallback(async () => { + const panelsData = await getPanelsData(selectedDashboards); + for (const selectedDashboard of selectedDashboards) { + try { + await dashboardService.attachPanels( + selectedDashboard.id, + selectedDashboard.attributes, + panelsData + ); + toasts.success({ + title: ( + + ), + toastLifeTimeMs: 3000, + }); + } catch (e) { + toasts.danger({ + body: e, + }); + } + } + }, [selectedDashboards, getPanelsData]); + + const addToDashboardAndEditCallback = useCallback(async () => { + onClose(async () => { + await addToDashboardCallback(); + const selectedDashboardId = selectedDashboards[0].id; + await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); + }); + }, [addToDashboardCallback, selectedDashboards, navigateToUrl]); + + return { addToDashboardCallback, addToDashboardAndEditCallback }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx new file mode 100644 index 0000000000000..8721de497eedc --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiInMemoryTableProps } from '@elastic/eui'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { debounce } from 'lodash'; +import type { DashboardSavedObject } from '../../../../../../../src/plugins/dashboard/public'; +import { useDashboardService } from '../../services/dashboard_service'; +import { useMlKibana } from '../../contexts/kibana'; + +export interface DashboardItem { + id: string; + title: string; + description: string | undefined; + attributes: DashboardSavedObject; +} + +export type EuiTableProps = EuiInMemoryTableProps; + +export const useDashboardTable = () => { + const { + notifications: { toasts }, + } = useMlKibana(); + + const dashboardService = useDashboardService(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + fetchDashboards(); + + return () => { + fetchDashboards.cancel(); + }; + }, []); + + const search: EuiTableProps['search'] = useMemo(() => { + return { + onChange: ({ queryText }) => { + setIsLoading(true); + fetchDashboards(queryText); + }, + box: { + incremental: true, + 'data-test-subj': 'mlDashboardsSearchBox', + }, + }; + }, []); + + const [dashboardItems, setDashboardItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); + + const fetchDashboards = useCallback( + debounce(async (query?: string) => { + try { + const response = await dashboardService.fetchDashboards(query); + const items: DashboardItem[] = response.savedObjects.map((savedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + description: savedObject.attributes.description, + attributes: savedObject.attributes, + }; + }); + setDashboardItems(items); + } catch (e) { + toasts.danger({ + body: e, + }); + } + setIsLoading(false); + }, 500), + [] + ); + const selection: EuiTableProps['selection'] = { + onSelectionChange: setSelectedItems, + }; + return { dashboardItems, selectedItems, selection, search, isLoading }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 6979277c43077..45665b2026db5 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -72,6 +72,7 @@ import { getToastNotifications } from '../util/dependency_cache'; import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; import { ML_APP_URL_GENERATOR } from '../../../common/constants/ml_url_generator'; +import { AnomalyContextMenu } from './anomaly_context_menu'; const ExplorerPage = ({ children, @@ -431,14 +432,32 @@ export class ExplorerUI extends React.Component { )} {loading === false && ( - -

- + + +

+ +

+
+
+ + + -

-
+ + { + explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA }); + }, clearInfluencerFilterSettings: () => { explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS }); }, @@ -137,6 +140,9 @@ export const explorerService = { setFilterData: (payload: Partial>) => { explorerAction$.next(setFilterDataActionCreator(payload)); }, + setChartsDataLoading: () => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS_DATA_LOADING }); + }, setSwimlaneContainerWidth: (payload: number) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH, 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 9e24a4349584e..b410449218d02 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 @@ -12,6 +12,7 @@ import { TimeRangeBounds } from '../util/time_buckets'; import { RecordForInfluencer } from '../services/results_service/results_service'; import { InfluencersFilterQuery } from '../../../common/types/es_client'; import { MlResultsService } from '../services/results_service'; +import { EntityField } from '../../../common/util/anomaly_utils'; interface ClearedSelectedAnomaliesState { selectedCells: undefined; @@ -60,7 +61,7 @@ export declare const getSelectionJobIds: ( export declare const getSelectionInfluencers: ( selectedCells: AppStateSelectedCells | undefined, fieldName: string -) => string[]; +) => EntityField[]; interface SelectionTimeRange { earliestMs: number; @@ -149,6 +150,7 @@ export declare const loadDataForCharts: ( ) => Promise; export declare const loadFilteredTopInfluencers: ( + mlResultsService: MlResultsService, jobIds: string[], 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 ea101d104f783..69bdac060a2dc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -536,65 +536,6 @@ export async function loadAnomaliesTableData( }); } -// track the request to be able to ignore out of date requests -// and avoid race conditions ending up with the wrong charts. -let requestCount = 0; -export async function loadDataForCharts( - mlResultsService, - jobIds, - earliestMs, - latestMs, - influencers = [], - selectedCells, - influencersFilterQuery, - // choose whether or not to keep track of the request that could be out of date - // in Anomaly Explorer this is being used to ignore any request that are out of date - // but in embeddables, we might have multiple requests coming from multiple different panels - takeLatestOnly = true -) { - return new Promise((resolve) => { - // Just skip doing the request when this function - // is called without the minimum required data. - if ( - selectedCells === undefined && - influencers.length === 0 && - influencersFilterQuery === undefined - ) { - resolve([]); - } - - const newRequestCount = ++requestCount; - requestCount = newRequestCount; - - // Load the top anomalies (by record_score) which will be displayed in the charts. - mlResultsService - .getRecordsForInfluencer( - jobIds, - influencers, - 0, - earliestMs, - latestMs, - 500, - influencersFilterQuery - ) - .then((resp) => { - // Ignore this response if it's returned by an out of date promise - if (takeLatestOnly && newRequestCount < requestCount) { - resolve([]); - } - - if ( - (selectedCells !== undefined && Object.keys(selectedCells).length > 0) || - influencersFilterQuery !== undefined - ) { - resolve(resp.records); - } - - resolve([]); - }); - }); -} - export async function loadTopInfluencers( mlResultsService, selectedJobIds, 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 f66cd94314608..15e0caa29af39 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 @@ -20,7 +20,7 @@ import { import { checkSelectedCells } from './check_selected_cells'; import { clearInfluencerFilterSettings } from './clear_influencer_filter_settings'; import { jobSelectionChange } from './job_selection_change'; -import { ExplorerState } from './state'; +import { ExplorerState, getExplorerDefaultState } from './state'; import { setInfluencerFilterSettings } from './set_influencer_filter_settings'; import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder'; import { getTimeBoundsFromSelection } from '../../hooks/use_selected_cells'; @@ -31,6 +31,10 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo let nextState: ExplorerState; switch (type) { + case EXPLORER_ACTION.CLEAR_EXPLORER_DATA: + nextState = getExplorerDefaultState(); + break; + case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS: nextState = clearInfluencerFilterSettings(state); break; @@ -49,6 +53,14 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo nextState = jobSelectionChange(state, payload); break; + case EXPLORER_ACTION.SET_CHARTS_DATA_LOADING: + nextState = { + ...state, + anomalyChartsDataLoading: true, + chartsData: getDefaultChartsData(), + }; + break; + case EXPLORER_ACTION.SET_CHARTS: nextState = { ...state, 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 bb90fedfc2315..e9527b7c232e5 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 @@ -28,6 +28,7 @@ import { InfluencersFilterQuery } from '../../../../../common/types/es_client'; export interface ExplorerState { annotations: AnnotationsTable; + anomalyChartsDataLoading: boolean; chartsData: ExplorerChartsData; fieldFormatsLoading: boolean; filterActive: boolean; @@ -69,6 +70,7 @@ export function getExplorerDefaultState(): ExplorerState { annotationsData: [], aggregations: {}, }, + anomalyChartsDataLoading: true, chartsData: getDefaultChartsData(), fieldFormatsLoading: false, filterActive: false, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index 251b1b24087fa..f8195f5747f7e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -39,16 +39,6 @@ export function ResultLinks({ jobs }) { const singleMetricDisabledMessage = jobs.length === 1 && jobs[0].isNotSingleMetricViewerJobMessage; - const singleMetricDisabledMessageText = - singleMetricDisabledMessage !== undefined - ? i18n.translate('xpack.ml.jobsList.resultActions.singleMetricDisabledMessageText', { - defaultMessage: 'Disabled because {reason}.', - values: { - reason: singleMetricDisabledMessage, - }, - }) - : undefined; - const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true; const { createLinkWithUserDefaults } = useCreateADLinks(); const timeSeriesExplorerLink = useMemo( @@ -62,7 +52,7 @@ export function ResultLinks({ jobs }) { {singleMetricVisible && ( = ({ jobsWithTim } }, [JSON.stringify(jobIds)]); + useEffect(() => { + return () => { + // upon component unmounting + // clear any data to prevent next page from rendering old charts + explorerService.clearExplorerData(); + }; + }, []); + /** * TODO get rid of the intermediate state in explorerService. * URL state should be the only source of truth for related props. diff --git a/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts index 21f07ed9e5a3c..28140038d249b 100644 --- a/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/__mocks__/anomaly_explorer_charts_service.ts @@ -10,4 +10,5 @@ export const createAnomalyExplorerChartsServiceMock = () => ({ getAnomalyData: jest.fn(), setTimeRange: jest.fn(), getTimeBounds: jest.fn(), + loadDataForCharts$: jest.fn(), }); diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts index 36e18b49cfa84..ac61e11b1128e 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.test.ts @@ -13,7 +13,6 @@ import { of } from 'rxjs'; import { cloneDeep } from 'lodash'; import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { ExplorerChartsData } from '../explorer/explorer_charts/explorer_charts_container_service'; -import type { ExplorerService } from '../explorer/explorer_dashboard_service'; import type { MlApiServices } from './ml_api_service'; import type { MlResultsService } from './results_service'; import { getDefaultChartsData } from '../explorer/explorer_charts/explorer_charts_container_service'; @@ -89,9 +88,6 @@ describe('AnomalyExplorerChartsService', () => { (mlApiServicesMock as unknown) as MlApiServices, (mlResultsServiceMock as unknown) as MlResultsService ); - const explorerService = { - setCharts: jest.fn(), - }; const timeRange = { earliestMs: 1486656000000, @@ -104,13 +100,8 @@ describe('AnomalyExplorerChartsService', () => { ); }); - afterEach(() => { - explorerService.setCharts.mockClear(); - }); - test('should return anomaly data without explorer service', async () => { const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, (combinedJobRecords as unknown) as Record, 1000, mockAnomalyChartRecords, @@ -123,27 +114,8 @@ describe('AnomalyExplorerChartsService', () => { assertAnomalyDataResult(anomalyData); }); - test('should set anomaly data with explorer service side effects', async () => { - await anomalyExplorerService.getAnomalyData( - (explorerService as unknown) as ExplorerService, - (combinedJobRecords as unknown) as Record, - 1000, - mockAnomalyChartRecords, - timeRange.earliestMs, - timeRange.latestMs, - timefilterMock, - 0, - 12 - ); - - expect(explorerService.setCharts.mock.calls.length).toBe(2); - assertAnomalyDataResult(explorerService.setCharts.mock.calls[0][0]); - assertAnomalyDataResult(explorerService.setCharts.mock.calls[1][0]); - }); - test('call anomalyChangeListener with empty series config', async () => { const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, // @ts-ignore (combinedJobRecords as unknown) as Record, 1000, @@ -165,7 +137,6 @@ describe('AnomalyExplorerChartsService', () => { mockAnomalyChartRecordsClone[1].partition_field_value = 'AAL.'; const anomalyData = (await anomalyExplorerService.getAnomalyData( - undefined, (combinedJobRecords as unknown) as Record, 1000, mockAnomalyChartRecordsClone, diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index 72de5d003d4b8..70b7632775bf5 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -7,6 +7,8 @@ import { each, find, get, map, reduce, sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { Observable, of } from 'rxjs'; +import { map as mapObservable } from 'rxjs/operators'; import { RecordForInfluencer } from './results_service/results_service'; import { isMappableJob, @@ -29,7 +31,6 @@ import { CHART_TYPE, ChartType } from '../explorer/explorer_constants'; import type { ChartRecord } from '../explorer/explorer_utils'; import { RecordsForCriteria, ScheduledEventsByBucket } from './results_service/result_service_rx'; import { isPopulatedObject } from '../../../common/util/object_utils'; -import type { ExplorerService } from '../explorer/explorer_dashboard_service'; import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { ExplorerChartsData, @@ -37,6 +38,8 @@ import { } from '../explorer/explorer_charts/explorer_charts_container_service'; import { TimeRangeBounds } from '../util/time_buckets'; import { isDefined } from '../../../common/types/guards'; +import { AppStateSelectedCells } from '../explorer/explorer_utils'; +import { InfluencersFilterQuery } from '../../../common/types/es_client'; const CHART_MAX_POINTS = 500; const ANOMALIES_MAX_RESULTS = 500; const MAX_SCHEDULED_EVENTS = 10; // Max number of scheduled events displayed per bucket. @@ -370,15 +373,53 @@ export class AnomalyExplorerChartsService { // Getting only necessary job config and datafeed config without the stats jobIds.map((jobId) => this.mlApiServices.jobs.jobForCloning(jobId)) ); - const combinedJobs = combinedResults + return combinedResults .filter(isDefined) .filter((r) => r.job !== undefined && r.datafeed !== undefined) .map(({ job, datafeed }) => ({ ...job, datafeed_config: datafeed } as CombinedJob)); - return combinedJobs; + } + + public loadDataForCharts$( + jobIds: string[], + earliestMs: number, + latestMs: number, + influencers: EntityField[] = [], + selectedCells: AppStateSelectedCells | undefined, + influencersFilterQuery: InfluencersFilterQuery + ): Observable { + if ( + selectedCells === undefined && + influencers.length === 0 && + influencersFilterQuery === undefined + ) { + of([]); + } + + return this.mlResultsService + .getRecordsForInfluencer$( + jobIds, + influencers, + 0, + earliestMs, + latestMs, + 500, + influencersFilterQuery + ) + .pipe( + mapObservable((resp): RecordForInfluencer[] => { + if ( + (selectedCells !== undefined && Object.keys(selectedCells).length > 0) || + influencersFilterQuery !== undefined + ) { + return resp.records; + } + + return [] as RecordForInfluencer[]; + }) + ); } public async getAnomalyData( - explorerService: ExplorerService | undefined, combinedJobRecords: Record, chartsContainerWidth: number, anomalyRecords: ChartRecord[] | undefined, @@ -486,9 +527,6 @@ export class AnomalyExplorerChartsService { data.errorMessages = errorMessages; } - if (explorerService) { - explorerService.setCharts({ ...data }); - } if (seriesConfigs.length === 0) { return data; } @@ -711,9 +749,11 @@ export class AnomalyExplorerChartsService { // plus anomalyScore for points with anomaly markers. let chartData: ChartPoint[] = []; if (metricData !== undefined) { - if (eventDistribution.length > 0 && records.length > 0) { + if (records.length > 0) { const filterField = records[0].by_field_value || records[0].over_field_value; - chartData = eventDistribution.filter((d: { entity: any }) => d.entity !== filterField); + if (eventDistribution.length > 0) { + chartData = eventDistribution.filter((d: { entity: any }) => d.entity !== filterField); + } map(metricData, (value, time) => { // The filtering for rare/event_distribution charts needs to be handled // differently because of how the source data is structured. @@ -848,9 +888,6 @@ export class AnomalyExplorerChartsService { // push map data in if it's available data.seriesToPlot.push(...mapData); } - if (explorerService) { - explorerService.setCharts({ ...data }); - } return Promise.resolve(data); }) .catch((error) => { @@ -860,7 +897,7 @@ export class AnomalyExplorerChartsService { } public processRecordsForDisplay( - jobRecords: Record, + combinedJobRecords: Record, anomalyRecords: RecordForInfluencer[] ): { records: ChartRecord[]; errors: Record> | undefined } { // Aggregate the anomaly data by detector, and entity (by/over/partition). @@ -875,7 +912,7 @@ export class AnomalyExplorerChartsService { // Check if we can plot a chart for this record, depending on whether the source data // is chartable, and if model plot is enabled for the job. - const job = jobRecords[record.job_id]; + const job = combinedJobRecords[record.job_id]; // if we already know this job has datafeed aggregations we cannot support // no need to do more checks diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index e07d49ca23d3b..c31194b58d589 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -22,9 +22,12 @@ import { MlApiServices } from '../ml_api_service'; import { CriteriaField } from './index'; import { findAggField } from '../../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils'; -import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; +import { aggregationTypeTransform, EntityField } from '../../../../common/util/anomaly_utils'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { isPopulatedObject } from '../../../../common/util/object_utils'; +import { InfluencersFilterQuery } from '../../../../common/types/es_client'; +import { RecordForInfluencer } from './results_service'; +import { isRuntimeMappings } from '../../../../common'; interface ResultResponse { success: boolean; @@ -138,9 +141,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { }, }, size: 0, - _source: { - excludes: [], - }, + _source: false, aggs: { byTime: { date_histogram: { @@ -150,6 +151,9 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { }, }, }, + ...(isRuntimeMappings(datafeedConfig?.runtime_mappings) + ? { runtime_mappings: datafeedConfig?.runtime_mappings } + : {}), }; if (shouldCriteria.length > 0) { @@ -633,5 +637,135 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { latestMs ); }, + + // Queries Elasticsearch to obtain the record level results containing the specified influencer(s), + // for the specified job(s), time range, and record score threshold. + // influencers parameter must be an array, with each object in the array having 'fieldName' + // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, + // so this returns record level results which have at least one of the influencers. + // Pass an empty array or ['*'] to search over all job IDs. + getRecordsForInfluencer$( + jobIds: string[], + influencers: EntityField[], + threshold: number, + earliestMs: number, + latestMs: number, + maxResults: number, + influencersFilterQuery: InfluencersFilterQuery + ): Observable<{ records: RecordForInfluencer[]; success: boolean }> { + const obj = { success: true, records: [] as RecordForInfluencer[] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + if (influencersFilterQuery !== undefined) { + boolCriteria.push(influencersFilterQuery); + } + + // Add a nested query to filter for each of the specified influencers. + if (influencers.length > 0) { + boolCriteria.push({ + bool: { + should: influencers.map((influencer) => { + return { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': influencer.fieldName, + }, + }, + { + match: { + 'influencers.influencer_field_values': influencer.fieldValue, + }, + }, + ], + }, + }, + }, + }; + }), + minimum_should_match: 1, + }, + }); + } + + return mlApiServices.results + .anomalySearch$( + { + size: maxResults !== undefined ? maxResults : 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }, + jobIds + ) + .pipe( + map((resp) => { + if (resp.hits.total.value > 0) { + each(resp.hits.hits, (hit) => { + obj.records.push(hit._source); + }); + } + return obj; + }) + ); + }, }; } 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 d26e650d145cb..6161eeb4e7940 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 @@ -55,7 +55,6 @@ export function resultsServiceProvider( influencersFilterQuery: InfluencersFilterQuery ): Promise; getRecordInfluencers(): Promise; - getRecordsForInfluencer(): Promise; getRecordsForDetector(): Promise; getRecords(): Promise; getEventRateData( 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 b041267f46c04..c258d07cab484 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 @@ -779,139 +779,6 @@ export function resultsServiceProvider(mlApiServices) { }); }, - // Queries Elasticsearch to obtain the record level results containing the specified influencer(s), - // for the specified job(s), time range, and record score threshold. - // influencers parameter must be an array, with each object in the array having 'fieldName' - // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, - // so this returns record level results which have at least one of the influencers. - // Pass an empty array or ['*'] to search over all job IDs. - getRecordsForInfluencer( - jobIds, - influencers, - threshold, - earliestMs, - latestMs, - maxResults, - influencersFilterQuery - ) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis', - }, - }, - }, - { - range: { - record_score: { - gte: threshold, - }, - }, - }, - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr, - }, - }); - } - - if (influencersFilterQuery !== undefined) { - boolCriteria.push(influencersFilterQuery); - } - - // Add a nested query to filter for each of the specified influencers. - if (influencers.length > 0) { - boolCriteria.push({ - bool: { - should: influencers.map((influencer) => { - return { - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - match: { - 'influencers.influencer_field_name': influencer.fieldName, - }, - }, - { - match: { - 'influencers.influencer_field_values': influencer.fieldValue, - }, - }, - ], - }, - }, - }, - }; - }), - minimum_should_match: 1, - }, - }); - } - - mlApiServices.results - .anomalySearch( - { - size: maxResults !== undefined ? maxResults : 100, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false, - }, - }, - { - bool: { - must: boolCriteria, - }, - }, - ], - }, - }, - sort: [{ record_score: { order: 'desc' } }], - }, - }, - jobIds - ) - .then((resp) => { - if (resp.hits.total.value > 0) { - each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); - }, - // Queries Elasticsearch to obtain the record level results for the specified job and detector, // time range, record score threshold, and whether to only return results containing influencers. // An additional, optional influencer field name and value may also be provided. @@ -1039,14 +906,6 @@ export function resultsServiceProvider(mlApiServices) { }); }, - // Queries Elasticsearch to obtain all the record level results for the specified job(s), time range, - // and record score threshold. - // Pass an empty array or ['*'] to search over all job IDs. - // Returned response contains a records property, which is an array of the matching results. - getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { - return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults); - }, - // Queries Elasticsearch to obtain event rate data i.e. the count // of documents over time. // index can be a String, or String[], of index names to search. diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx index f32446fd6d9ab..a36d063737704 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_initializer.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyChartsEmbeddableInput } from '..'; import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../application/services/anomaly_explorer_charts_service'; -const MAX_SERIES_ALLOWED = 48; +export const MAX_ANOMALY_CHARTS_ALLOWED = 48; export interface AnomalyChartsInitializerProps { defaultTitle: string; initialInput?: Partial>; @@ -98,7 +98,7 @@ export const AnomalyChartsInitializer: FC = ({ value={maxSeriesToPlot} onChange={(e) => setMaxSeriesToPlot(parseInt(e.target.value, 10))} min={0} - max={MAX_SERIES_ALLOWED} + max={MAX_ANOMALY_CHARTS_ALLOWED} /> diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts index efac51edda69f..7045b2eac378a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.test.ts @@ -29,41 +29,6 @@ jest.mock('../../application/explorer/explorer_utils', () => ({ }), getSelectionJobIds: jest.fn(() => ['test-job']), getSelectionTimeRange: jest.fn(() => ({ earliestMs: 1521309543000, latestMs: 1616003942999 })), - loadDataForCharts: jest.fn().mockImplementation(() => - Promise.resolve([ - { - job_id: 'cw_multi_1', - result_type: 'record', - probability: 6.057139142746412e-13, - multi_bucket_impact: -5, - record_score: 89.71961, - initial_record_score: 98.36826274948001, - bucket_span: 900, - detector_index: 0, - is_interim: false, - timestamp: 1572892200000, - partition_field_name: 'instance', - partition_field_value: 'i-d17dcd4c', - function: 'mean', - function_description: 'mean', - typical: [1.6177685422858146], - actual: [7.235333333333333], - field_name: 'CPUUtilization', - influencers: [ - { - influencer_field_name: 'region', - influencer_field_values: ['sa-east-1'], - }, - { - influencer_field_name: 'instance', - influencer_field_values: ['i-d17dcd4c'], - }, - ], - instance: ['i-d17dcd4c'], - region: ['sa-east-1'], - }, - ]) - ), })); describe('useAnomalyChartsInputResolver', () => { @@ -115,6 +80,42 @@ describe('useAnomalyChartsInputResolver', () => { }) ); + anomalyExplorerChartsServiceMock.loadDataForCharts$.mockImplementation(() => + Promise.resolve([ + { + job_id: 'cw_multi_1', + result_type: 'record', + probability: 6.057139142746412e-13, + multi_bucket_impact: -5, + record_score: 89.71961, + initial_record_score: 98.36826274948001, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1572892200000, + partition_field_name: 'instance', + partition_field_value: 'i-d17dcd4c', + function: 'mean', + function_description: 'mean', + typical: [1.6177685422858146], + actual: [7.235333333333333], + field_name: 'CPUUtilization', + influencers: [ + { + influencer_field_name: 'region', + influencer_field_values: ['sa-east-1'], + }, + { + influencer_field_name: 'instance', + influencer_field_values: ['i-d17dcd4c'], + }, + ], + instance: ['i-d17dcd4c'], + region: ['sa-east-1'], + }, + ]) + ); + const coreStartMock = createCoreStartMock(); const mlStartMock = createMlStartDepsMock(); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts index b114ca89a3288..703851f3fe9b6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/use_anomaly_charts_input_resolver.ts @@ -18,7 +18,6 @@ import { getSelectionInfluencers, getSelectionJobIds, getSelectionTimeRange, - loadDataForCharts, } from '../../application/explorer/explorer_utils'; import { OVERALL_LABEL, SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; import { parseInterval } from '../../../common/util/parse_interval'; @@ -46,7 +45,7 @@ export function useAnomalyChartsInputResolver( const [ { uiSettings }, { data: dataServices }, - { anomalyDetectorService, anomalyExplorerService, mlResultsService }, + { anomalyDetectorService, anomalyExplorerService }, ] = services; const { timefilter } = dataServices.query.timefilter; @@ -125,15 +124,13 @@ export function useAnomalyChartsInputResolver( const timeRange = getSelectionTimeRange(selections, bucketInterval.asSeconds(), bounds); return forkJoin({ combinedJobs: anomalyExplorerService.getCombinedJobs(jobIds), - anomalyChartRecords: loadDataForCharts( - mlResultsService, + anomalyChartRecords: anomalyExplorerService.loadDataForCharts$( jobIds, timeRange.earliestMs, timeRange.latestMs, selectionInfluencers, selections, - influencersFilterQuery, - false + influencersFilterQuery ), }).pipe( switchMap(({ combinedJobs, anomalyChartRecords }) => { @@ -147,7 +144,6 @@ export function useAnomalyChartsInputResolver( return forkJoin({ chartsData: from( anomalyExplorerService.getAnomalyData( - undefined, combinedJobRecords, embeddableContainerWidth, anomalyChartRecords, diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 106bc807ff996..ff033dbb33043 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -97,23 +97,6 @@ export const CALCULATE_DURATION_UNTIL = 'until'; */ export const ML_SUPPORTED_LICENSES = ['trial', 'platinum', 'enterprise']; -/** - * Metadata service URLs for the different cloud services that have constant URLs (e.g., unlike GCP, which is a constant prefix). - * - * @type {Object} - */ -export const CLOUD_METADATA_SERVICES = { - // We explicitly call out the version, 2016-09-02, rather than 'latest' to avoid unexpected changes - AWS_URL: 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document', - - // 2017-04-02 is the first GA release of this API - AZURE_URL: 'http://169.254.169.254/metadata/instance?api-version=2017-04-02', - - // GCP documentation shows both 'metadata.google.internal' (mostly) and '169.254.169.254' (sometimes) - // To bypass potential DNS changes, the IP was used because it's shared with other cloud services - GCP_URL_PREFIX: 'http://169.254.169.254/computeMetadata/v1/instance', -}; - /** * Constants used by Logstash monitoring code */ diff --git a/x-pack/plugins/monitoring/server/cloud/aws.js b/x-pack/plugins/monitoring/server/cloud/aws.js deleted file mode 100644 index 45b3b80162875..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/aws.js +++ /dev/null @@ -1,127 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get, isString, omit } from 'lodash'; -import { promisify } from 'util'; -import { CloudService } from './cloud_service'; -import { CloudServiceResponse } from './cloud_response'; -import fs from 'fs'; -import { CLOUD_METADATA_SERVICES } from '../../common/constants'; - -/** - * {@code AWSCloudService} will check and load the service metadata for an Amazon Web Service VM if it is available. - * - * This is exported for testing purposes. Use the {@code AWS} singleton. - */ -export class AWSCloudService extends CloudService { - constructor(options = {}) { - super('aws', options); - - // Allow the file system handler to be swapped out for tests - const { _fs = fs, _isWindows = process.platform.startsWith('win') } = options; - - this._fs = _fs; - this._isWindows = _isWindows; - } - - _checkIfService(request) { - const req = { - method: 'GET', - uri: CLOUD_METADATA_SERVICES.AWS_URL, - json: true, - }; - - return ( - promisify(request)(req) - .then((response) => this._parseResponse(response.body, (body) => this._parseBody(body))) - // fall back to file detection - .catch(() => this._tryToDetectUuid()) - ); - } - - /** - * Parse the AWS response, if possible. Example payload (with fake accountId value): - * - * { - * "devpayProductCodes" : null, - * "privateIp" : "10.0.0.38", - * "availabilityZone" : "us-west-2c", - * "version" : "2010-08-31", - * "instanceId" : "i-0c7a5b7590a4d811c", - * "billingProducts" : null, - * "instanceType" : "t2.micro", - * "imageId" : "ami-6df1e514", - * "accountId" : "1234567890", - * "architecture" : "x86_64", - * "kernelId" : null, - * "ramdiskId" : null, - * "pendingTime" : "2017-07-06T02:09:12Z", - * "region" : "us-west-2" - * } - * - * @param {Object} body The response from the VM web service. - * @return {CloudServiceResponse} {@code null} if not confirmed. Otherwise the response. - */ - _parseBody(body) { - const id = get(body, 'instanceId'); - const vmType = get(body, 'instanceType'); - const region = get(body, 'region'); - const zone = get(body, 'availabilityZone'); - const metadata = omit(body, [ - // remove keys we already have - 'instanceId', - 'instanceType', - 'region', - 'availabilityZone', - // remove keys that give too much detail - 'accountId', - 'billingProducts', - 'devpayProductCodes', - 'privateIp', - ]); - - // ensure we actually have some data - if (id || vmType || region || zone) { - return new CloudServiceResponse(this._name, true, { id, vmType, region, zone, metadata }); - } - - return null; - } - - /** - * Attempt to load the UUID by checking `/sys/hypervisor/uuid`. This is a fallback option if the metadata service is - * unavailable for some reason. - * - * @return {Promise} Never {@code null} {@code CloudServiceResponse}. - */ - _tryToDetectUuid() { - // Windows does not have an easy way to check - if (!this._isWindows) { - return promisify(this._fs.readFile)('/sys/hypervisor/uuid', 'utf8').then((uuid) => { - if (isString(uuid)) { - // Some AWS APIs return it lowercase (like the file did in testing), while others return it uppercase - uuid = uuid.trim().toLowerCase(); - - if (uuid.startsWith('ec2')) { - return new CloudServiceResponse(this._name, true, { id: uuid }); - } - } - - return this._createUnconfirmedResponse(); - }); - } - - return Promise.resolve(this._createUnconfirmedResponse()); - } -} - -/** - * Singleton instance of {@code AWSCloudService}. - * - * @type {AWSCloudService} - */ -export const AWS = new AWSCloudService(); diff --git a/x-pack/plugins/monitoring/server/cloud/aws.test.js b/x-pack/plugins/monitoring/server/cloud/aws.test.js deleted file mode 100644 index 877a1958f0096..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/aws.test.js +++ /dev/null @@ -1,237 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AWS, AWSCloudService } from './aws'; - -describe('AWS', () => { - const expectedFilename = '/sys/hypervisor/uuid'; - const expectedEncoding = 'utf8'; - // mixed case to ensure we check for ec2 after lowercasing - const ec2Uuid = 'eC2abcdef-ghijk\n'; - const ec2FileSystem = { - readFile: (filename, encoding, callback) => { - expect(filename).toEqual(expectedFilename); - expect(encoding).toEqual(expectedEncoding); - - callback(null, ec2Uuid); - }, - }; - - it('is named "aws"', () => { - expect(AWS.getName()).toEqual('aws'); - }); - - describe('_checkIfService', () => { - it('handles expected response', async () => { - const id = 'abcdef'; - const request = (req, callback) => { - expect(req.method).toEqual('GET'); - expect(req.uri).toEqual( - 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document' - ); - expect(req.json).toEqual(true); - - const body = `{"instanceId": "${id}","availabilityZone":"us-fake-2c", "imageId" : "ami-6df1e514"}`; - - callback(null, { statusCode: 200, body }, body); - }; - // ensure it does not use the fs to trump the body - const awsCheckedFileSystem = new AWSCloudService({ - _fs: ec2FileSystem, - _isWindows: false, - }); - - const response = await awsCheckedFileSystem._checkIfService(request); - - expect(response.isConfirmed()).toEqual(true); - expect(response.toJSON()).toEqual({ - name: AWS.getName(), - id, - region: undefined, - vm_type: undefined, - zone: 'us-fake-2c', - metadata: { - imageId: 'ami-6df1e514', - }, - }); - }); - - it('handles request without a usable body by downgrading to UUID detection', async () => { - const request = (_req, callback) => callback(null, { statusCode: 404 }); - const awsCheckedFileSystem = new AWSCloudService({ - _fs: ec2FileSystem, - _isWindows: false, - }); - - const response = await awsCheckedFileSystem._checkIfService(request); - - expect(response.isConfirmed()).toBe(true); - expect(response.toJSON()).toEqual({ - name: AWS.getName(), - id: ec2Uuid.trim().toLowerCase(), - region: undefined, - vm_type: undefined, - zone: undefined, - metadata: undefined, - }); - }); - - it('handles request failure by downgrading to UUID detection', async () => { - const failedRequest = (_req, callback) => - callback(new Error('expected: request failed'), null); - const awsCheckedFileSystem = new AWSCloudService({ - _fs: ec2FileSystem, - _isWindows: false, - }); - - const response = await awsCheckedFileSystem._checkIfService(failedRequest); - - expect(response.isConfirmed()).toBe(true); - expect(response.toJSON()).toEqual({ - name: AWS.getName(), - id: ec2Uuid.trim().toLowerCase(), - region: undefined, - vm_type: undefined, - zone: undefined, - metadata: undefined, - }); - }); - - it('handles not running on AWS', async () => { - const failedRequest = (_req, callback) => callback(null, null); - const awsIgnoredFileSystem = new AWSCloudService({ - _fs: ec2FileSystem, - _isWindows: true, - }); - - const response = await awsIgnoredFileSystem._checkIfService(failedRequest); - - expect(response.getName()).toEqual(AWS.getName()); - expect(response.isConfirmed()).toBe(false); - }); - }); - - describe('_parseBody', () => { - it('parses object in expected format', () => { - const body = { - devpayProductCodes: null, - privateIp: '10.0.0.38', - availabilityZone: 'us-west-2c', - version: '2010-08-31', - instanceId: 'i-0c7a5b7590a4d811c', - billingProducts: null, - instanceType: 't2.micro', - accountId: '1234567890', - architecture: 'x86_64', - kernelId: null, - ramdiskId: null, - imageId: 'ami-6df1e514', - pendingTime: '2017-07-06T02:09:12Z', - region: 'us-west-2', - }; - - const response = AWS._parseBody(body); - - expect(response.getName()).toEqual(AWS.getName()); - expect(response.isConfirmed()).toEqual(true); - expect(response.toJSON()).toEqual({ - name: 'aws', - id: 'i-0c7a5b7590a4d811c', - vm_type: 't2.micro', - region: 'us-west-2', - zone: 'us-west-2c', - metadata: { - version: '2010-08-31', - architecture: 'x86_64', - kernelId: null, - ramdiskId: null, - imageId: 'ami-6df1e514', - pendingTime: '2017-07-06T02:09:12Z', - }, - }); - }); - - it('ignores unexpected response body', () => { - expect(AWS._parseBody(undefined)).toBe(null); - expect(AWS._parseBody(null)).toBe(null); - expect(AWS._parseBody({})).toBe(null); - expect(AWS._parseBody({ privateIp: 'a.b.c.d' })).toBe(null); - }); - }); - - describe('_tryToDetectUuid', () => { - it('checks the file system for UUID if not Windows', async () => { - const awsCheckedFileSystem = new AWSCloudService({ - _fs: ec2FileSystem, - _isWindows: false, - }); - - const response = await awsCheckedFileSystem._tryToDetectUuid(); - - expect(response.isConfirmed()).toEqual(true); - expect(response.toJSON()).toEqual({ - name: AWS.getName(), - id: ec2Uuid.trim().toLowerCase(), - region: undefined, - zone: undefined, - vm_type: undefined, - metadata: undefined, - }); - }); - - it('ignores UUID if it does not start with ec2', async () => { - const notEC2FileSystem = { - readFile: (filename, encoding, callback) => { - expect(filename).toEqual(expectedFilename); - expect(encoding).toEqual(expectedEncoding); - - callback(null, 'notEC2'); - }, - }; - - const awsCheckedFileSystem = new AWSCloudService({ - _fs: notEC2FileSystem, - _isWindows: false, - }); - - const response = await awsCheckedFileSystem._tryToDetectUuid(); - - expect(response.isConfirmed()).toEqual(false); - }); - - it('does NOT check the file system for UUID on Windows', async () => { - const awsUncheckedFileSystem = new AWSCloudService({ - _fs: ec2FileSystem, - _isWindows: true, - }); - - const response = await awsUncheckedFileSystem._tryToDetectUuid(); - - expect(response.isConfirmed()).toEqual(false); - }); - - it('does NOT handle file system exceptions', async () => { - const fileDNE = new Error('File DNE'); - const awsFailedFileSystem = new AWSCloudService({ - _fs: { - readFile: () => { - throw fileDNE; - }, - }, - _isWindows: false, - }); - - try { - await awsFailedFileSystem._tryToDetectUuid(); - - expect().fail('Method should throw exception (Promise.reject)'); - } catch (err) { - expect(err).toBe(fileDNE); - } - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/cloud/azure.js b/x-pack/plugins/monitoring/server/cloud/azure.js deleted file mode 100644 index 4d026441d6840..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/azure.js +++ /dev/null @@ -1,99 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get, omit } from 'lodash'; -import { promisify } from 'util'; -import { CloudService } from './cloud_service'; -import { CloudServiceResponse } from './cloud_response'; -import { CLOUD_METADATA_SERVICES } from '../../common/constants'; - -/** - * {@code AzureCloudService} will check and load the service metadata for an Azure VM if it is available. - */ -class AzureCloudService extends CloudService { - constructor(options = {}) { - super('azure', options); - } - - _checkIfService(request) { - const req = { - method: 'GET', - uri: CLOUD_METADATA_SERVICES.AZURE_URL, - headers: { - // Azure requires this header - Metadata: 'true', - }, - json: true, - }; - - return ( - promisify(request)(req) - // Note: there is no fallback option for Azure - .then((response) => { - return this._parseResponse(response.body, (body) => this._parseBody(body)); - }) - ); - } - - /** - * Parse the Azure response, if possible. Example payload (with network object ignored): - * - * { - * "compute": { - * "location": "eastus", - * "name": "my-ubuntu-vm", - * "offer": "UbuntuServer", - * "osType": "Linux", - * "platformFaultDomain": "0", - * "platformUpdateDomain": "0", - * "publisher": "Canonical", - * "sku": "16.04-LTS", - * "version": "16.04.201706191", - * "vmId": "d4c57456-2b3b-437a-9f1f-7082cfce02d4", - * "vmSize": "Standard_A1" - * }, - * "network": { - * ... - * } - * } - * - * Note: Azure VMs created using the "classic" method, as opposed to the resource manager, - * do not provide a "compute" field / object. However, both report the "network" field / object. - * - * @param {Object} body The response from the VM web service. - * @return {CloudServiceResponse} {@code null} for default fallback. - */ - _parseBody(body) { - const compute = get(body, 'compute'); - const id = get(compute, 'vmId'); - const vmType = get(compute, 'vmSize'); - const region = get(compute, 'location'); - - // remove keys that we already have; explicitly undefined so we don't send it when empty - const metadata = compute ? omit(compute, ['vmId', 'vmSize', 'location']) : undefined; - - // we don't actually use network, but we check for its existence to see if this is a response from Azure - const network = get(body, 'network'); - - // ensure we actually have some data - if (id || vmType || region) { - return new CloudServiceResponse(this._name, true, { id, vmType, region, metadata }); - } else if (network) { - // classic-managed VMs in Azure don't provide compute so we highlight the lack of info - return new CloudServiceResponse(this._name, true, { metadata: { classic: true } }); - } - - return null; - } -} - -/** - * Singleton instance of {@code AzureCloudService}. - * - * @type {AzureCloudService} - */ -export const AZURE = new AzureCloudService(); diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_detector.js b/x-pack/plugins/monitoring/server/cloud/cloud_detector.js deleted file mode 100644 index 2cd2b26daab5b..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/cloud_detector.js +++ /dev/null @@ -1,64 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CLOUD_SERVICES } from './cloud_services'; - -/** - * {@code CloudDetector} can be used to asynchronously detect the cloud service that Kibana is running within. - */ -export class CloudDetector { - constructor(options = {}) { - const { cloudServices = CLOUD_SERVICES } = options; - - this._cloudServices = cloudServices; - // Explicitly undefined. If the value is never updated, then the property will be dropped when the data is serialized. - this._cloudDetails = undefined; - } - - /** - * Get any cloud details that we have detected. - * - * @return {Object} {@code undefined} if unknown. Otherwise plain JSON. - */ - getCloudDetails() { - return this._cloudDetails; - } - - /** - * Asynchronously detect the cloud service. - * - * Callers are _not_ expected to {@code await} this method, which allows the caller to trigger the lookup and then simply use it - * whenever we determine it. - */ - async detectCloudService() { - this._cloudDetails = await this._getCloudService(this._cloudServices); - } - - /** - * Check every cloud service until the first one reports success from detection. - * - * @param {Array} cloudServices The {@code CloudService} objects listed in priority order - * @return {Promise} {@code undefined} if none match. Otherwise the plain JSON {@code Object} from the {@code CloudServiceResponse}. - */ - async _getCloudService(cloudServices) { - // check each service until we find one that is confirmed to match; order is assumed to matter - for (const service of cloudServices) { - try { - const serviceResponse = await service.checkIfService(); - - if (serviceResponse.isConfirmed()) { - return serviceResponse.toJSON(); - } - } catch (ignoredError) { - // ignored until we make wider use of this in the UI - } - } - - // explicitly undefined rather than null so that it can be ignored in JSON - return undefined; - } -} diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_service.js b/x-pack/plugins/monitoring/server/cloud/cloud_service.js deleted file mode 100644 index ea0eb9534cf30..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/cloud_service.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isObject, isString } from 'lodash'; -import request from 'request'; -import { CloudServiceResponse } from './cloud_response'; - -/** - * {@code CloudService} provides a mechanism for cloud services to be checked for metadata - * that may help to determine the best defaults and priorities. - */ -export class CloudService { - constructor(name, options = {}) { - this._name = name.toLowerCase(); - - // Allow the HTTP handler to be swapped out for tests - const { _request = request } = options; - - this._request = _request; - } - - /** - * Get the search-friendly name of the Cloud Service. - * - * @return {String} Never {@code null}. - */ - getName() { - return this._name; - } - - /** - * Using whatever mechanism is required by the current Cloud Service, determine - * Kibana is running in it and return relevant metadata. - * - * @return {Promise} Never {@code null} {@code CloudServiceResponse}. - */ - checkIfService() { - return this._checkIfService(this._request).catch(() => this._createUnconfirmedResponse()); - } - - /** - * Using whatever mechanism is required by the current Cloud Service, determine - * Kibana is running in it and return relevant metadata. - * - * @param {Object} _request 'request' HTTP handler. - * @return {Promise} Never {@code null} {@code CloudServiceResponse}. - */ - _checkIfService() { - return Promise.reject(new Error('not implemented')); - } - - /** - * Create a new {@code CloudServiceResponse} that denotes that this cloud service is not being used by the current machine / VM. - * - * @return {CloudServiceResponse} Never {@code null}. - */ - _createUnconfirmedResponse() { - return CloudServiceResponse.unconfirmed(this._name); - } - - /** - * Strictly parse JSON. - * - * @param {String} value The string to parse as a JSON object - * @return {Object} The result of {@code JSON.parse} if it's an object. - * @throws {Error} if the {@code value} is not a String that can be converted into an Object - */ - _stringToJson(value) { - // note: this will throw an error if this is not a string - value = value.trim(); - - // we don't want to return scalar values, arrays, etc. - if (value.startsWith('{') && value.endsWith('}')) { - return JSON.parse(value); - } - - throw new Error(`'${value}' is not a JSON object`); - } - - /** - * Convert the {@code response} to a JSON object and attempt to parse it using the {@code parseBody} function. - * - * If the {@code response} cannot be parsed as a JSON object, or if it fails to be useful, then {@code parseBody} should return - * {@code null}. - * - * @param {Object} body The body from the response from the VM web service. - * @param {Function} parseBody Single argument function that accepts parsed JSON body from the response. - * @return {Promise} Never {@code null} {@code CloudServiceResponse} or rejection. - */ - _parseResponse(body, parseBody) { - // parse it if necessary - if (isString(body)) { - try { - body = this._stringToJson(body); - } catch (err) { - return Promise.reject(err); - } - } - - if (isObject(body)) { - const response = parseBody(body); - - if (response) { - return Promise.resolve(response); - } - } - - // use default handling - return Promise.reject(); - } -} diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_services.js b/x-pack/plugins/monitoring/server/cloud/cloud_services.js deleted file mode 100644 index 23be0d0e20e25..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/cloud_services.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AWS } from './aws'; -import { AZURE } from './azure'; -import { GCP } from './gcp'; - -/** - * An iteratable that can be used to loop across all known cloud services to detect them. - * - * @type {Array} - */ -export const CLOUD_SERVICES = [AWS, GCP, AZURE]; diff --git a/x-pack/plugins/monitoring/server/cloud/cloud_services.test.js b/x-pack/plugins/monitoring/server/cloud/cloud_services.test.js deleted file mode 100644 index adf4bf2bb0f0f..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/cloud_services.test.js +++ /dev/null @@ -1,22 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CLOUD_SERVICES } from './cloud_services'; -import { AWS } from './aws'; -import { AZURE } from './azure'; -import { GCP } from './gcp'; - -describe('cloudServices', () => { - const expectedOrder = [AWS, GCP, AZURE]; - - it('iterates in expected order', () => { - let i = 0; - for (const service of CLOUD_SERVICES) { - expect(service).toBe(expectedOrder[i++]); - } - }); -}); diff --git a/x-pack/plugins/monitoring/server/cloud/gcp.js b/x-pack/plugins/monitoring/server/cloud/gcp.js deleted file mode 100644 index ab8935769b312..0000000000000 --- a/x-pack/plugins/monitoring/server/cloud/gcp.js +++ /dev/null @@ -1,136 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isString } from 'lodash'; -import { promisify } from 'util'; -import { CloudService } from './cloud_service'; -import { CloudServiceResponse } from './cloud_response'; -import { CLOUD_METADATA_SERVICES } from '../../common/constants'; - -/** - * {@code GCPCloudService} will check and load the service metadata for an Google Cloud Platform VM if it is available. - */ -class GCPCloudService extends CloudService { - constructor(options = {}) { - super('gcp', options); - } - - _checkIfService(request) { - // we need to call GCP individually for each field - const fields = ['id', 'machine-type', 'zone']; - - const create = this._createRequestForField; - const allRequests = fields.map((field) => promisify(request)(create(field))); - return ( - Promise.all(allRequests) - /* - Note: there is no fallback option for GCP; - responses are arrays containing [fullResponse, body]; - because GCP returns plaintext, we have no way of validating without using the response code - */ - .then((responses) => { - return responses.map((response) => { - return this._extractBody(response, response.body); - }); - }) - .then(([id, machineType, zone]) => this._combineResponses(id, machineType, zone)) - ); - } - - _createRequestForField(field) { - return { - method: 'GET', - uri: `${CLOUD_METADATA_SERVICES.GCP_URL_PREFIX}/${field}`, - headers: { - // GCP requires this header - 'Metadata-Flavor': 'Google', - }, - // GCP does _not_ return JSON - json: false, - }; - } - - /** - * Extract the body if the response is valid and it came from GCP. - * - * @param {Object} response The response object - * @param {Object} body The response body, if any - * @return {Object} {@code body} (probably actually a String) if the response came from GCP. Otherwise {@code null}. - */ - _extractBody(response, body) { - if ( - response && - response.statusCode === 200 && - response.headers && - response.headers['metadata-flavor'] === 'Google' - ) { - return body; - } - - return null; - } - - /** - * Parse the GCP responses, if possible. Example values for each parameter: - * - * {@code vmId}: '5702733457649812345' - * {@code machineType}: 'projects/441331612345/machineTypes/f1-micro' - * {@code zone}: 'projects/441331612345/zones/us-east4-c' - * - * @param {String} vmId The ID of the VM - * @param {String} machineType The machine type, prefixed by unwanted account info. - * @param {String} zone The zone (e.g., availability zone), implicitly showing the region, prefixed by unwanted account info. - * @return {CloudServiceResponse} Never {@code null}. - * @throws {Error} if the responses do not make a valid response - */ - _combineResponses(id, machineType, zone) { - const vmId = isString(id) ? id.trim() : null; - const vmType = this._extractValue('machineTypes/', machineType); - const vmZone = this._extractValue('zones/', zone); - - let region; - - if (vmZone) { - // converts 'us-east4-c' into 'us-east4' - region = vmZone.substring(0, vmZone.lastIndexOf('-')); - } - - // ensure we actually have some data - if (vmId || vmType || region || vmZone) { - return new CloudServiceResponse(this._name, true, { id: vmId, vmType, region, zone: vmZone }); - } - - throw new Error('unrecognized responses'); - } - - /** - * Extract the useful information returned from GCP while discarding unwanted account details (the project ID). For example, - * this turns something like 'projects/441331612345/machineTypes/f1-micro' into 'f1-micro'. - * - * @param {String} fieldPrefix The value prefixing the actual value of interest. - * @param {String} value The entire value returned from GCP. - * @return {String} {@code undefined} if the value could not be extracted. Otherwise just the desired value. - */ - _extractValue(fieldPrefix, value) { - if (isString(value)) { - const index = value.lastIndexOf(fieldPrefix); - - if (index !== -1) { - return value.substring(index + fieldPrefix.length).trim(); - } - } - - return undefined; - } -} - -/** - * Singleton instance of {@code GCPCloudService}. - * - * @type {GCPCloudService} - */ -export const GCP = new GCPCloudService(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts similarity index 76% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts index a31679c61a4ab..7af3252584819 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; -import { OperationType } from '../../../../../../lens/public'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -20,11 +19,11 @@ export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigPr sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average', sourceField: 'transaction.duration.us', label: 'Latency', }, - hasMetricType: true, + hasOperationType: true, defaultFilters: [ 'user_agent.name', 'user_agent.os.name', @@ -37,7 +36,7 @@ export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigPr 'client.geo.country_name', 'user_agent.device.name', ], - filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)], + filters: buildPhraseFilter('transaction.type', 'request', indexPattern), labels: { ...FieldLabels }, reportDefinitions: [ { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts similarity index 74% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts index 32cae2167ddf0..7b1d472ac8bbf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; -import { OperationType } from '../../../../../../lens/public'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants/constants'; +import { buildPhraseFilter } from '../utils'; export function getServiceThroughputLensConfig({ seriesId, @@ -16,18 +15,18 @@ export function getServiceThroughputLensConfig({ }: ConfigProps): DataSeries { return { id: seriesId, - reportType: 'service-latency', + reportType: 'service-throughput', defaultSeriesType: 'line', seriesTypes: ['line', 'bar'], xAxisColumn: { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average', sourceField: 'transaction.duration.us', label: 'Throughput', }, - hasMetricType: true, + hasOperationType: true, defaultFilters: [ 'user_agent.name', 'user_agent.os.name', @@ -40,7 +39,7 @@ export function getServiceThroughputLensConfig({ 'client.geo.country_name', 'user_agent.device.name', ], - filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)], + filters: buildPhraseFilter('transaction.type', 'request', indexPattern), labels: { ...FieldLabels }, reportDefinitions: [ { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts similarity index 78% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index aa3ac2fa64317..14cd24c42e6a2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { AppDataType, ReportViewTypeId } from '../types'; -import { - CLS_FIELD, - FCP_FIELD, - FID_FIELD, - LCP_FIELD, - TBT_FIELD, -} from './data/elasticsearch_fieldnames'; +import { AppDataType, ReportViewTypeId } from '../../types'; +import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; + +export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; export const FieldLabels: Record = { 'user_agent.name': 'Browser family', @@ -24,10 +20,10 @@ export const FieldLabels: Record = { 'service.name': 'Service Name', 'service.environment': 'Environment', - [LCP_FIELD]: 'Largest contentful paint', - [FCP_FIELD]: 'First contentful paint', - [TBT_FIELD]: 'Total blocking time', - [FID_FIELD]: 'First input delay', + [LCP_FIELD]: 'Largest contentful paint (Seconds)', + [FCP_FIELD]: 'First contentful paint (Seconds)', + [TBT_FIELD]: 'Total blocking time (Seconds)', + [FID_FIELD]: 'First input delay (Seconds)', [CLS_FIELD]: 'Cumulative layout shift', 'monitor.id': 'Monitor Id', @@ -38,6 +34,7 @@ export const FieldLabels: Record = { 'monitor.name': 'Monitor name', 'monitor.type': 'Monitor Type', 'url.port': 'Port', + 'url.full': 'Url', tags: 'Tags', // custom diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts similarity index 72% rename from x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts index 650a0a9b82391..63661f0d5a996 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/index.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export * from './location_map'; -export * from '../availability_reporting/location_status_tags'; +export * from './constants'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts similarity index 94% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index 5b99c19dbabb7..67d72a656744c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -6,7 +6,7 @@ */ export enum URL_KEYS { - METRIC_TYPE = 'mt', + OPERATION_TYPE = 'op', REPORT_TYPE = 'rt', SERIES_TYPE = 'st', BREAK_DOWN = 'bd', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 85d48ef638d44..2c5b4ebea0ab3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -6,16 +6,16 @@ */ import { ReportViewTypes } from '../types'; -import { getPerformanceDistLensConfig } from './performance_dist_config'; -import { getMonitorDurationConfig } from './monitor_duration_config'; -import { getServiceLatencyLensConfig } from './service_latency_config'; -import { getMonitorPingsConfig } from './monitor_pings_config'; -import { getServiceThroughputLensConfig } from './service_throughput_config'; -import { getKPITrendsLensConfig } from './kpi_trends_config'; -import { getCPUUsageLensConfig } from './cpu_usage_config'; -import { getMemoryUsageLensConfig } from './memory_usage_config'; -import { getNetworkActivityLensConfig } from './network_activity_config'; -import { getLogsFrequencyLensConfig } from './logs_frequency_config'; +import { getPerformanceDistLensConfig } from './rum/performance_dist_config'; +import { getMonitorDurationConfig } from './synthetics/monitor_duration_config'; +import { getServiceLatencyLensConfig } from './apm/service_latency_config'; +import { getMonitorPingsConfig } from './synthetics/monitor_pings_config'; +import { getServiceThroughputLensConfig } from './apm/service_throughput_config'; +import { getKPITrendsLensConfig } from './rum/kpi_trends_config'; +import { getCPUUsageLensConfig } from './metrics/cpu_usage_config'; +import { getMemoryUsageLensConfig } from './metrics/memory_usage_config'; +import { getNetworkActivityLensConfig } from './metrics/network_activity_config'; +import { getLogsFrequencyLensConfig } from './logs/logs_frequency_config'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; interface Props { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts deleted file mode 100644 index a967a8824bca7..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts +++ /dev/null @@ -1,73 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; -import { - CLIENT_GEO_COUNTRY_NAME, - PROCESSOR_EVENT, - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_TYPE, - USER_AGENT_DEVICE, - USER_AGENT_NAME, - USER_AGENT_OS, - USER_AGENT_VERSION, -} from './data/elasticsearch_fieldnames'; - -export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { - return { - id: seriesId, - defaultSeriesType: 'bar_stacked', - reportType: 'kpi-trends', - seriesTypes: ['bar', 'bar_stacked'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumn: { - operationType: 'count', - label: 'Page views', - }, - hasMetricType: false, - defaultFilters: [ - USER_AGENT_OS, - CLIENT_GEO_COUNTRY_NAME, - USER_AGENT_DEVICE, - { - field: USER_AGENT_NAME, - nested: USER_AGENT_VERSION, - }, - ], - breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], - filters: [ - buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), - buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), - ], - labels: { ...FieldLabels, SERVICE_NAME: 'Web Application' }, - reportDefinitions: [ - { - field: SERVICE_NAME, - required: true, - }, - { - field: SERVICE_ENVIRONMENT, - }, - { - field: 'Business.KPI', - custom: true, - defaultValue: 'Records', - options: [ - { - field: 'Records', - label: 'Page views', - }, - ], - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index dcfaed938cc0f..0de78c45041d4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -8,9 +8,8 @@ import { LensAttributes } from './lens_attributes'; import { mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; -import { sampleAttribute } from './data/sample_attribute'; -import { LCP_FIELD, SERVICE_NAME } from './data/elasticsearch_fieldnames'; -import { USER_AGENT_NAME } from './data/elasticsearch_fieldnames'; +import { sampleAttribute } from './test_data/sample_attribute'; +import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; describe('Lens Attribute', () => { const reportViewConfig = getDefaultConfigs({ @@ -43,14 +42,18 @@ describe('Lens Attribute', () => { it('should return expected field type', function () { expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual( JSON.stringify({ - count: 0, - name: 'transaction.type', - type: 'string', - esTypes: ['keyword'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, + fieldMeta: { + count: 0, + name: 'transaction.type', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + fieldName: 'transaction.type', + columnType: null, }) ); }); @@ -58,14 +61,18 @@ describe('Lens Attribute', () => { it('should return expected field type for custom field with default value', function () { expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( JSON.stringify({ - count: 0, - name: 'transaction.duration.us', - type: 'number', - esTypes: ['long'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, + fieldMeta: { + count: 0, + name: 'transaction.duration.us', + type: 'number', + esTypes: ['long'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + fieldName: 'transaction.duration.us', + columnType: null, }) ); }); @@ -77,23 +84,48 @@ describe('Lens Attribute', () => { expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( JSON.stringify({ - count: 0, - name: LCP_FIELD, - type: 'number', - esTypes: ['scaled_float'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, + fieldMeta: { + count: 0, + name: LCP_FIELD, + type: 'number', + esTypes: ['scaled_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + fieldName: LCP_FIELD, }) ); }); - it('should return expected number column', function () { - expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({ + it('should return expected number range column', function () { + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + dataType: 'number', + isBucketed: true, + label: 'Page load time (Seconds)', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }); + }); + + it('should return expected number operation column', function () { + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -129,7 +161,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getXAxis()).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -154,7 +186,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -318,7 +350,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', @@ -363,7 +395,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Page load time (Seconds)', operationType: 'range', params: { maxBars: 'auto', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 589a93d160068..12a5b19fb02fc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -5,10 +5,14 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import { capitalize } from 'lodash'; import { CountIndexPatternColumn, DateHistogramIndexPatternColumn, - LastValueIndexPatternColumn, + AvgIndexPatternColumn, + MedianIndexPatternColumn, + PercentileIndexPatternColumn, OperationType, PersistedIndexPatternLayer, RangeIndexPatternColumn, @@ -17,6 +21,8 @@ import { XYState, XYCurveType, DataType, + OperationMetadata, + FieldBasedIndexPatternColumn, } from '../../../../../../lens/public'; import { buildPhraseFilter, @@ -30,6 +36,15 @@ function getLayerReferenceName(layerId: string) { return `indexpattern-datasource-layer-${layerId}`; } +function buildNumberColumn(sourceField: string) { + return { + sourceField, + dataType: 'number' as DataType, + isBucketed: false, + scale: 'ratio' as OperationMetadata['scale'], + }; +} + export class LensAttributes { indexPattern: IndexPattern; layers: Record; @@ -44,7 +59,7 @@ export class LensAttributes { reportViewConfig: DataSeries, seriesType?: SeriesType, filters?: UrlFilter[], - metricType?: OperationType, + operationType?: OperationType, reportDefinitions?: Record ) { this.indexPattern = indexPattern; @@ -52,8 +67,8 @@ export class LensAttributes { this.filters = filters ?? []; this.reportDefinitions = reportDefinitions ?? {}; - if (typeof reportViewConfig.yAxisColumn.operationType !== undefined && metricType) { - reportViewConfig.yAxisColumn.operationType = metricType; + if (typeof reportViewConfig.yAxisColumn.operationType !== undefined && operationType) { + reportViewConfig.yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; } this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType; this.reportViewConfig = reportViewConfig; @@ -93,7 +108,7 @@ export class LensAttributes { this.visualization.layers[0].splitAccessor = undefined; } - getNumberColumn(sourceField: string): RangeIndexPatternColumn { + getNumberRangeColumn(sourceField: string): RangeIndexPatternColumn { return { sourceField, label: this.reportViewConfig.labels[sourceField], @@ -109,6 +124,38 @@ export class LensAttributes { }; } + getNumberOperationColumn( + sourceField: string, + operationType: 'average' | 'median' + ): AvgIndexPatternColumn | MedianIndexPatternColumn { + return { + ...buildNumberColumn(sourceField), + label: i18n.translate('xpack.observability.expView.columns.operation.label', { + defaultMessage: '{operationType} of {sourceField}', + values: { + sourceField: this.reportViewConfig.labels[sourceField], + operationType: capitalize(operationType), + }, + }), + operationType, + }; + } + + getPercentileNumberColumn( + sourceField: string, + percentileValue: string + ): PercentileIndexPatternColumn { + return { + ...buildNumberColumn(sourceField), + label: i18n.translate('xpack.observability.expView.columns.label', { + defaultMessage: '{percentileValue} percentile of {sourceField}', + values: { sourceField, percentileValue }, + }), + operationType: 'percentile', + params: { percentile: Number(percentileValue.split('th')[0]) }, + }; + } + getDateHistogramColumn(sourceField: string): DateHistogramIndexPatternColumn { return { sourceField, @@ -121,56 +168,89 @@ export class LensAttributes { }; } - getXAxis(): - | LastValueIndexPatternColumn - | DateHistogramIndexPatternColumn - | RangeIndexPatternColumn { + getXAxis() { const { xAxisColumn } = this.reportViewConfig; - const { type: fieldType, name: fieldName } = this.getFieldMeta(xAxisColumn.sourceField)!; + return this.getColumnBasedOnType(xAxisColumn.sourceField!); + } + + getColumnBasedOnType(sourceField: string, operationType?: OperationType) { + const { fieldMeta, columnType, fieldName } = this.getFieldMeta(sourceField); + const { type: fieldType } = fieldMeta ?? {}; + + if (fieldName === 'Records') { + return this.getRecordsColumn(); + } if (fieldType === 'date') { return this.getDateHistogramColumn(fieldName); } if (fieldType === 'number') { - return this.getNumberColumn(fieldName); + if (columnType === 'operation' || operationType) { + if (operationType === 'median' || operationType === 'average') { + return this.getNumberOperationColumn(fieldName, operationType); + } + if (operationType?.includes('th')) { + return this.getPercentileNumberColumn(sourceField, operationType); + } + } + return this.getNumberRangeColumn(fieldName); } // FIXME review my approach again return this.getDateHistogramColumn(fieldName); } - getFieldMeta(sourceField?: string) { - let xAxisField = sourceField; + getCustomFieldName(sourceField: string) { + let fieldName = sourceField; + let columnType = null; - if (xAxisField) { - const rdf = this.reportViewConfig.reportDefinitions ?? []; + const rdf = this.reportViewConfig.reportDefinitions ?? []; - const customField = rdf.find(({ field }) => field === xAxisField); + const customField = rdf.find(({ field }) => field === fieldName); - if (customField) { - if (this.reportDefinitions[xAxisField]) { - xAxisField = this.reportDefinitions[xAxisField]; - } else if (customField.defaultValue) { - xAxisField = customField.defaultValue; - } else if (customField.options?.[0].field) { - xAxisField = customField.options?.[0].field; - } + if (customField) { + if (this.reportDefinitions[fieldName]) { + fieldName = this.reportDefinitions[fieldName]; + if (customField?.options) + columnType = customField?.options?.find(({ field }) => field === fieldName)?.columnType; + } else if (customField.defaultValue) { + fieldName = customField.defaultValue; + } else if (customField.options?.[0].field) { + fieldName = customField.options?.[0].field; + columnType = customField.options?.[0].columnType; } - - return this.indexPattern.getFieldByName(xAxisField); } + + return { fieldName, columnType }; + } + + getFieldMeta(sourceField: string) { + const { fieldName, columnType } = this.getCustomFieldName(sourceField); + + const fieldMeta = this.indexPattern.getFieldByName(fieldName); + + return { fieldMeta, fieldName, columnType }; } getMainYAxis() { + const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumn; + + if (sourceField === 'Records' || !sourceField) { + return this.getRecordsColumn(label); + } + + return this.getColumnBasedOnType(sourceField!, operationType); + } + + getRecordsColumn(label?: string): CountIndexPatternColumn { return { dataType: 'number', isBucketed: false, - label: 'Count of records', + label: label || 'Count of records', operationType: 'count', scale: 'ratio', sourceField: 'Records', - ...this.reportViewConfig.yAxisColumn, } as CountIndexPatternColumn; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts similarity index 87% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts index 68e5e697d2f9d..9f8a336b59d34 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; @@ -24,7 +24,7 @@ export function getLogsFrequencyLensConfig({ seriesId }: Props): DataSeries { yAxisColumn: { operationType: 'count', }, - hasMetricType: false, + hasOperationType: false, defaultFilters: [], breakdowns: ['agent.hostname'], filters: [], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts similarity index 80% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts index 5a4fb2aa3a6a5..d4b807de11f4e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; @@ -23,11 +22,11 @@ export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average', sourceField: 'system.cpu.user.pct', label: 'CPU Usage %', }, - hasMetricType: true, + hasOperationType: true, defaultFilters: [], breakdowns: ['host.hostname'], filters: [], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts similarity index 80% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts index 579372ed86fa7..38d1c425fc09a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; @@ -23,11 +22,11 @@ export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average', sourceField: 'system.memory.used.pct', label: 'Memory Usage %', }, - hasMetricType: true, + hasOperationType: true, defaultFilters: [], breakdowns: ['host.hostname'], filters: [], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts similarity index 80% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts index 63cdd0ec8bd60..07a521225b38d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; @@ -23,10 +22,10 @@ export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average', sourceField: 'system.memory.used.pct', }, - hasMetricType: true, + hasOperationType: true, defaultFilters: [], breakdowns: ['host.hostname'], filters: [], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts new file mode 100644 index 0000000000000..f1fc5f310b8ef --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldFormat } from '../../types'; +import { + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, + TRANSACTION_DURATION, +} from '../constants/elasticsearch_fieldnames'; + +export const rumFieldFormats: FieldFormat[] = [ + { + field: TRANSACTION_DURATION, + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asSeconds', + showSuffix: true, + outputPrecision: 1, + }, + }, + }, + { + field: FCP_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: LCP_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: TBT_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, + { + field: FID_FIELD, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'asSeconds', + showSuffix: true, + }, + }, + }, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts new file mode 100644 index 0000000000000..cd38d912850cf --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; +import { + CLIENT_GEO_COUNTRY_NAME, + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TBT_FIELD, + TRANSACTION_DURATION, + TRANSACTION_TYPE, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, + USER_AGENT_VERSION, + TRANSACTION_TIME_TO_FIRST_BYTE, +} from '../constants/elasticsearch_fieldnames'; + +export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + return { + id: seriesId, + defaultSeriesType: 'bar_stacked', + reportType: 'kpi-trends', + seriesTypes: ['bar', 'bar_stacked'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + sourceField: 'business.kpi', + operationType: 'median', + }, + hasOperationType: false, + defaultFilters: [ + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + { + field: USER_AGENT_NAME, + nested: USER_AGENT_VERSION, + }, + ], + breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + filters: [ + ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), + ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), + ], + labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' }, + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + }, + { + field: 'business.kpi', + custom: true, + defaultValue: 'Records', + options: [ + { field: 'Records', label: 'Page views' }, + { label: 'Page load time', field: TRANSACTION_DURATION, columnType: 'operation' }, + { label: 'Backend time', field: TRANSACTION_TIME_TO_FIRST_BYTE, columnType: 'operation' }, + { label: 'First contentful paint', field: FCP_FIELD, columnType: 'operation' }, + { label: 'Total blocking time', field: TBT_FIELD, columnType: 'operation' }, + { label: 'Largest contentful paint', field: LCP_FIELD, columnType: 'operation' }, + { label: 'First input delay', field: FID_FIELD, columnType: 'operation' }, + { label: 'Cumulative layout shift', field: CLS_FIELD, columnType: 'operation' }, + ], + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts similarity index 79% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts index 41617304c9f3d..4b6d5dd6e741b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { buildPhraseFilter } from './utils'; +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; +import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, CLS_FIELD, @@ -19,12 +19,13 @@ import { SERVICE_NAME, TBT_FIELD, TRANSACTION_DURATION, + TRANSACTION_TIME_TO_FIRST_BYTE, TRANSACTION_TYPE, USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, USER_AGENT_VERSION, -} from './data/elasticsearch_fieldnames'; +} from '../constants/elasticsearch_fieldnames'; export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -36,10 +37,10 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP sourceField: 'performance.metric', }, yAxisColumn: { - operationType: 'count', + sourceField: 'Records', label: 'Pages loaded', }, - hasMetricType: false, + hasOperationType: false, defaultFilters: [ USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, @@ -64,6 +65,7 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP defaultValue: TRANSACTION_DURATION, options: [ { label: 'Page load time', field: TRANSACTION_DURATION }, + { label: 'Backend time', field: TRANSACTION_TIME_TO_FIRST_BYTE }, { label: 'First contentful paint', field: FCP_FIELD }, { label: 'Total blocking time', field: TBT_FIELD }, // FIXME, review if we need these descriptions @@ -74,13 +76,13 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP }, ], filters: [ - buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), - buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), + ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), + ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application', - [TRANSACTION_DURATION]: 'Page load time', + [TRANSACTION_DURATION]: 'Page load time (Seconds)', }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts new file mode 100644 index 0000000000000..8dad1839f0bcd --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldFormat } from '../../types'; + +export const syntheticsFieldFormats: FieldFormat[] = [ + { + field: 'monitor.duration.us', + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asMilliseconds', + outputPrecision: 0, + showSuffix: true, + }, + }, + }, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts similarity index 82% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts index aa9b8b94c6d86..efbc3d14441c2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; -import { OperationType } from '../../../../../../lens/public'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; @@ -23,11 +22,11 @@ export function getMonitorDurationConfig({ seriesId }: Props): DataSeries { sourceField: '@timestamp', }, yAxisColumn: { - operationType: 'avg' as OperationType, + operationType: 'average', sourceField: 'monitor.duration.us', label: 'Monitor duration (ms)', }, - hasMetricType: true, + hasOperationType: true, defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'], breakdowns: [ 'observer.geo.name', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts similarity index 89% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts index 72968626e934b..68a36dcdcaf85 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { DataSeries } from '../types'; -import { FieldLabels } from './constants'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../constants'; interface Props { seriesId: string; @@ -25,7 +25,7 @@ export function getMonitorPingsConfig({ seriesId }: Props): DataSeries { operationType: 'count', label: 'Monitor pings', }, - hasMetricType: false, + hasOperationType: false, defaultFilters: ['observer.geo.name'], breakdowns: ['monitor.status', 'observer.geo.name', 'monitor.type'], filters: [], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts similarity index 98% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 9b299e7d70bcc..ffce81207472f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -21,7 +21,7 @@ export const sampleAttribute = { columns: { 'x-axis-column': { sourceField: 'transaction.duration.us', - label: 'Page load time', + label: 'Page load time (Seconds)', dataType: 'number', operationType: 'range', isBucketed: true, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/test_index_pattern.json diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 38b8ce81b2acd..c6b7b5d92d5f8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,15 +5,15 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { AllSeries, AllShortSeries } from '../hooks/use_url_strorage'; +import type { AllSeries, AllShortSeries } from '../hooks/use_url_storage'; import type { SeriesUrl } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; -import { URL_KEYS } from './url_constants'; +import { URL_KEYS } from './constants/url_constants'; export function convertToShortUrl(series: SeriesUrl) { const { - metric, + operationType, seriesType, reportType, breakdown, @@ -23,7 +23,7 @@ export function convertToShortUrl(series: SeriesUrl) { } = series; return { - [URL_KEYS.METRIC_TYPE]: metric, + [URL_KEYS.OPERATION_TYPE]: operationType, [URL_KEYS.REPORT_TYPE]: reportType, [URL_KEYS.SERIES_TYPE]: seriesType, [URL_KEYS.BREAK_DOWN]: breakdown, @@ -49,6 +49,9 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { } export function buildPhraseFilter(field: string, value: any, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field)!; - return esFilters.buildPhraseFilter(fieldMeta, value, indexPattern); + const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + if (fieldMeta) { + return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)]; + } + return []; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index b90d5115bc41e..257eb3a739f0f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -10,7 +10,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/dom'; import { render, mockUrlStorage, mockCore } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; -import * as obsvInd from '../../../utils/observability_index_patterns'; +import * as obsvInd from './utils/observability_index_patterns'; describe('ExploratoryView', () => { beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index b3ad107bbe0e2..6bc069aafa5b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -6,16 +6,14 @@ */ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; -import styled from 'styled-components'; -import { EuiLoadingSpinner, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiTitle } from '@elastic/eui'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; import { SeriesEditor } from './series_editor/series_editor'; -import { useUrlStorage } from './hooks/use_url_strorage'; +import { useUrlStorage } from './hooks/use_url_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { EmptyView } from './components/empty_view'; -import { useIndexPatternContext } from './hooks/use_default_index_pattern'; import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryView() { @@ -27,15 +25,12 @@ export function ExploratoryView() { null ); - const { indexPattern } = useIndexPatternContext(); - const LensComponent = lens?.EmbeddableComponent; const { firstSeriesId: seriesId, firstSeries: series } = useUrlStorage(); const lensAttributesT = useLensAttributes({ seriesId, - indexPattern, }); useEffect(() => { @@ -48,11 +43,6 @@ export function ExploratoryView() { {lens ? ( <> - {!indexPattern && ( - - - - )} {lensAttributes && seriesId && series?.reportType && series?.time ? ( ); } - -const SpinnerWrap = styled.div` - height: 100vh; - display: flex; - justify-content: center; - align-items: center; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index bda3566c76602..17f06436c8535 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -12,7 +12,7 @@ import { TypedLensByValueInput } from '../../../../../../lens/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { DataViewLabels } from '../configurations/constants'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx index 04cbb4a4ddb18..c5a4d02492662 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx @@ -10,7 +10,7 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { AppDataType } from '../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; -import { ObservabilityIndexPatterns } from '../../../../utils/observability_index_patterns'; +import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; export interface IIndexPatternContext { indexPattern: IndexPattern; @@ -39,6 +39,7 @@ export function IndexPatternContextProvider({ } = useKibana(); const loadIndexPattern = async (dataType: AppDataType) => { + setIndexPattern(undefined); const obsvIndexP = new ObservabilityIndexPatterns(data); const indPattern = await obsvIndexP.getIndexPattern(dataType); setIndexPattern(indPattern!); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts index 9f462790e8d37..de4343b290118 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts @@ -8,12 +8,9 @@ import { useFetcher } from '../../../..'; import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; -import { AllShortSeries } from './use_url_strorage'; +import { AllShortSeries } from './use_url_storage'; import { ReportToDataTypeMap } from '../configurations/constants'; -import { - DataType, - ObservabilityIndexPatterns, -} from '../../../../utils/observability_index_patterns'; +import { DataType, ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { const { @@ -30,15 +27,21 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { const firstSeries = allSeries[firstSeriesId]; - const { data: indexPattern } = useFetcher(() => { + let dataType: DataType = firstSeries?.dataType ?? 'rum'; + + if (firstSeries?.rt) { + dataType = ReportToDataTypeMap[firstSeries?.rt]; + } + + const { data: indexPattern, error } = useFetcher(() => { const obsvIndexP = new ObservabilityIndexPatterns(data); - let reportType: DataType = 'apm'; - if (firstSeries?.rt) { - reportType = ReportToDataTypeMap[firstSeries?.rt]; - } - return obsvIndexP.getIndexPattern(reportType); - }, [firstSeries?.rt, data]); + return obsvIndexP.getIndexPattern(dataType); + }, [dataType, data]); + + if (error) { + throw error; + } return indexPattern; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 1c735009f66f9..555b21618c4b2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -8,15 +8,14 @@ import { useMemo } from 'react'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LensAttributes } from '../configurations/lens_attributes'; -import { useUrlStorage } from './use_url_strorage'; +import { useUrlStorage } from './use_url_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { DataSeries, SeriesUrl, UrlFilter } from '../types'; +import { useIndexPatternContext } from './use_default_index_pattern'; interface Props { seriesId: string; - indexPattern?: IndexPattern | null; } export const getFiltersFromDefs = ( @@ -39,12 +38,12 @@ export const getFiltersFromDefs = ( export const useLensAttributes = ({ seriesId, - indexPattern, }: Props): TypedLensByValueInput['attributes'] | null => { const { series } = useUrlStorage(seriesId); - const { breakdown, seriesType, metric: metricType, reportType, reportDefinitions = {} } = - series ?? {}; + const { breakdown, seriesType, operationType, reportType, reportDefinitions = {} } = series ?? {}; + + const { indexPattern } = useIndexPatternContext(); return useMemo(() => { if (!indexPattern || !reportType) { @@ -66,7 +65,7 @@ export const useLensAttributes = ({ dataViewConfig, seriesType, filters, - metricType, + operationType, reportDefinitions ); @@ -79,7 +78,7 @@ export const useLensAttributes = ({ indexPattern, breakdown, seriesType, - metricType, + operationType, reportType, reportDefinitions, seriesId, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index 35247180c2ee5..34f0a7c1a7f86 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useUrlStorage } from './use_url_strorage'; +import { useUrlStorage } from './use_url_storage'; import { UrlFilter } from '../types'; export interface UpdateFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx similarity index 93% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx index d38429703b709..a4fe15025245a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx @@ -10,7 +10,7 @@ import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_ import type { AppDataType, ReportViewTypeId, SeriesUrl, UrlFilter } from '../types'; import { convertToShortUrl } from '../configurations/utils'; import { OperationType, SeriesType } from '../../../../../../lens/public'; -import { URL_KEYS } from '../configurations/url_constants'; +import { URL_KEYS } from '../configurations/constants/url_constants'; export const UrlStorageContext = createContext(null); @@ -26,9 +26,9 @@ export function UrlStorageContextProvider({ } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { mt, st, rt, bd, ft, time, rdf, ...restSeries } = newValue; + const { op, st, rt, bd, ft, time, rdf, ...restSeries } = newValue; return { - metric: mt, + operationType: op, reportType: rt!, seriesType: st, breakdown: bd, @@ -40,7 +40,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { } interface ShortUrlSeries { - [URL_KEYS.METRIC_TYPE]?: OperationType; + [URL_KEYS.OPERATION_TYPE]?: OperationType; [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index dc47a0f075fe6..f903c4d7d44fb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -18,7 +18,7 @@ import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../../../../../../src/plugins/kibana_utils/public/'; -import { UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { UrlStorageContextProvider } from './hooks/use_url_storage'; import { useInitExploratoryView } from './hooks/use_init_exploratory_view'; import { WithHeaderLayout } from '../../app/layout/with_header'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 112bfcc3ccb58..b826409dd9e3a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -23,20 +23,20 @@ import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { lensPluginMock } from '../../../../../lens/public/mocks'; import { IndexPatternContextProvider } from './hooks/use_default_index_pattern'; -import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_storage'; import { withNotifyOnErrors, createKbnUrlStateStorage, } from '../../../../../../../src/plugins/kibana_utils/public'; import * as fetcherHook from '../../../hooks/use_fetcher'; -import * as useUrlHook from './hooks/use_url_strorage'; +import * as useUrlHook from './hooks/use_url_storage'; import * as useSeriesFilterHook from './hooks/use_series_filters'; import * as useHasDataHook from '../../../hooks/use_has_data'; import * as useValuesListHook from '../../../hooks/use_values_list'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/index_patterns/index_pattern.stub'; -import indexPatternData from './configurations/data/test_index_pattern.json'; +import indexPatternData from './configurations/test_data/test_index_pattern.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx similarity index 74% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx index f291d0de4dac0..bac935dbecbe7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx @@ -7,14 +7,14 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { SeriesChartTypes, XYChartTypes } from './chart_types'; import { mockUrlStorage, render } from '../../rtl_helpers'; +import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types'; -describe.skip('SeriesChartTypes', function () { +describe.skip('SeriesChartTypesSelect', function () { it('should render properly', async function () { mockUrlStorage({}); - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -24,7 +24,7 @@ describe.skip('SeriesChartTypes', function () { it('should call set series on change', async function () { const { setSeries } = mockUrlStorage({}); - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -42,11 +42,11 @@ describe.skip('SeriesChartTypes', function () { expect(setSeries).toHaveBeenCalledTimes(3); }); - describe('XYChartTypes', function () { + describe('XYChartTypesSelect', function () { it('should render properly', async function () { mockUrlStorage({}); - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx new file mode 100644 index 0000000000000..029c39df13aad --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; +import { useFetcher } from '../../../../..'; +import { useUrlStorage } from '../../hooks/use_url_storage'; +import { SeriesType } from '../../../../../../../lens/public'; + +export function SeriesChartTypesSelect({ + seriesId, + defaultChartType, +}: { + seriesId: string; + defaultChartType: SeriesType; +}) { + const { series, setSeries, allSeries } = useUrlStorage(seriesId); + + const seriesType = series?.seriesType ?? defaultChartType; + + const onChange = (value: SeriesType) => { + Object.keys(allSeries).forEach((seriesKey) => { + const seriesN = allSeries[seriesKey]; + + setSeries(seriesKey, { ...seriesN, seriesType: value }); + }); + }; + + return ( + + ); +} + +export interface XYChartTypesProps { + label?: string; + value: SeriesType; + includeChartTypes?: SeriesType[]; + excludeChartTypes?: SeriesType[]; + onChange: (value: SeriesType) => void; +} + +export function XYChartTypesSelect({ + onChange, + value, + includeChartTypes, + excludeChartTypes, +}: XYChartTypesProps) { + const { + services: { lens }, + } = useKibana(); + + const { data = [], loading } = useFetcher(() => lens.getXyVisTypes(), [lens]); + + let vizTypes = data ?? []; + + if ((excludeChartTypes ?? []).length > 0) { + vizTypes = vizTypes.filter(({ id }) => !excludeChartTypes?.includes(id as SeriesType)); + } + + if ((includeChartTypes ?? []).length > 0) { + vizTypes = vizTypes.filter(({ id }) => includeChartTypes?.includes(id as SeriesType)); + } + + const options = (vizTypes ?? []).map(({ id, fullLabel, label, icon }) => { + const LabelWithIcon = ( + + + + + {fullLabel || label} + + ); + return { + value: id as SeriesType, + inputDisplay: LabelWithIcon, + dropdownDisplay: LabelWithIcon, + }; + }); + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index d33d8515d3bee..41b9f7d22ba00 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { mockUrlStorage, render } from '../../rtl_helpers'; import { dataTypes, DataTypesCol } from './data_types_col'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; describe('DataTypesCol', function () { it('should render properly', function () { @@ -32,7 +32,7 @@ describe('DataTypesCol', function () { }); it('should set series on change on already selected', function () { - const { setSeries } = mockUrlStorage({ + const { removeSeries } = mockUrlStorage({ data: { [NEW_SERIES_KEY]: { dataType: 'synthetics', @@ -54,6 +54,6 @@ describe('DataTypesCol', function () { fireEvent.click(button); // undefined on click selected - expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: undefined }); + expect(removeSeries).toHaveBeenCalledWith('newSeriesKey'); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 7ea44e66a721a..d7e90d34a2596 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AppDataType } from '../../types'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; export const dataTypes: Array<{ id: AppDataType; label: string }> = [ { id: 'synthetics', label: 'Synthetic Monitoring' }, @@ -20,15 +20,19 @@ export const dataTypes: Array<{ id: AppDataType; label: string }> = [ ]; export function DataTypesCol() { - const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY); + const { series, setSeries, removeSeries } = useUrlStorage(NEW_SERIES_KEY); - const { loadIndexPattern } = useIndexPatternContext(); + const { loadIndexPattern, indexPattern } = useIndexPatternContext(); const onDataTypeChange = (dataType?: AppDataType) => { if (dataType) { loadIndexPattern(dataType); } - setSeries(NEW_SERIES_KEY, { dataType } as any); + if (!dataType) { + removeSeries(NEW_SERIES_KEY); + } else { + setSeries(NEW_SERIES_KEY, { dataType } as any); + } }; const selectedDataType = series.dataType; @@ -43,6 +47,8 @@ export function DataTypesCol() { iconType="arrowRight" color={selectedDataType === dataTypeId ? 'primary' : 'text'} fill={selectedDataType === dataTypeId} + isDisabled={!indexPattern} + isLoading={!indexPattern && selectedDataType === dataTypeId} onClick={() => { onDataTypeChange(dataTypeId === selectedDataType ? undefined : dataTypeId); }} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx new file mode 100644 index 0000000000000..e05f91b4bb0bd --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockUrlStorage, render } from '../../rtl_helpers'; +import { OperationTypeSelect } from './operation_type_select'; + +describe('OperationTypeSelect', function () { + it('should render properly', function () { + render(); + + screen.getByText('Select an option: , is selected'); + }); + + it('should display selected value', function () { + mockUrlStorage({ + data: { + 'performance-distribution': { + reportType: 'kpi', + operationType: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + screen.getByText('Median'); + }); + + it('should call set series on change', function () { + const { setSeries } = mockUrlStorage({ + data: { + 'series-id': { + reportType: 'kpi', + operationType: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + fireEvent.click(screen.getByTestId('operationTypeSelect')); + + expect(setSeries).toHaveBeenCalledWith('series-id', { + operationType: 'median', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + + fireEvent.click(screen.getByText('95th Percentile')); + expect(setSeries).toHaveBeenCalledWith('series-id', { + operationType: '95th', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx new file mode 100644 index 0000000000000..46167af0b244a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSuperSelect } from '@elastic/eui'; + +import { useUrlStorage } from '../../hooks/use_url_storage'; +import { OperationType } from '../../../../../../../lens/public'; + +export function OperationTypeSelect({ + seriesId, + defaultOperationType, +}: { + seriesId: string; + defaultOperationType?: OperationType; +}) { + const { series, setSeries } = useUrlStorage(seriesId); + + const operationType = series?.operationType; + + const onChange = (value: OperationType) => { + setSeries(seriesId, { ...series, operationType: value }); + }; + + useEffect(() => { + setSeries(seriesId, { ...series, operationType: operationType || defaultOperationType }); + }, [defaultOperationType, seriesId, operationType, setSeries, series]); + + const options = [ + { + value: 'average' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.average', { + defaultMessage: 'Average', + }), + }, + { + value: 'median' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.median', { + defaultMessage: 'Median', + }), + }, + { + value: '75th' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.75thPercentile', { + defaultMessage: '75th Percentile', + }), + }, + { + value: '90th' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.90thPercentile', { + defaultMessage: '90th Percentile', + }), + }, + { + value: '95th' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.95thPercentile', { + defaultMessage: '95th Percentile', + }), + }, + { + value: '99th' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.99thPercentile', { + defaultMessage: '99th Percentile', + }), + }, + ]; + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index dba660fff9c36..553aff57ad491 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -10,9 +10,9 @@ import { fireEvent, screen } from '@testing-library/react'; import { render } from '../../../../../utils/test_helper'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { ReportBreakdowns } from './report_breakdowns'; -import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportBreakdowns', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx index 7667cea417a52..619e2ec4fe9b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Breakdowns } from '../../series_editor/columns/breakdowns'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { DataSeries } from '../../types'; export function ReportBreakdowns({ dataViewSeries }: { dataViewSeries: DataSeries }) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 2fda581154166..104a8fcefb49f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { mockIndexPattern, mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { ReportDefinitionCol } from './report_definition_col'; -import { SERVICE_NAME } from '../../configurations/data/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index ce11c869de0ab..a386b73a8f917 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -8,10 +8,12 @@ import React from 'react'; import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; import { CustomReportField } from '../custom_report_field'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { DataSeries } from '../../types'; +import { SeriesChartTypesSelect } from './chart_types'; +import { OperationTypeSelect } from './operation_type_select'; export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSeries }) { const { indexPattern } = useIndexPatternContext(); @@ -20,7 +22,14 @@ export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSe const { reportDefinitions: rtd = {} } = series; - const { reportDefinitions, labels, filters } = dataViewSeries; + const { + reportDefinitions, + labels, + filters, + defaultSeriesType, + hasOperationType, + yAxisColumn, + } = dataViewSeries; const onChange = (field: string, value?: string) => { if (!value) { @@ -67,6 +76,7 @@ export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSe {rtd?.[field] && ( ))} + + + + {hasOperationType && ( + + + + )} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index 674f5e6f49bde..4c9b5827e2bde 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -11,7 +11,7 @@ import { render } from '../../../../../utils/test_helper'; import { ReportFilters } from './report_filters'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; describe('Series Builder ReportFilters', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx index 903dda549aeee..24b24b76af1f6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { SeriesFilter } from '../../series_editor/columns/series_filter'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { DataSeries } from '../../types'; export function ReportFilters({ dataViewSeries }: { dataViewSeries: DataSeries }) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index 567e2654130e8..f845bf9885af9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -10,6 +10,7 @@ import { fireEvent, screen } from '@testing-library/react'; import { mockUrlStorage, render } from '../../rtl_helpers'; import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; import { ReportTypes } from '../series_builder'; +import { DEFAULT_TIME } from '../../configurations/constants'; describe('ReportTypesCol', function () { it('should render properly', function () { @@ -60,6 +61,9 @@ describe('ReportTypesCol', function () { fireEvent.click(button); // undefined on click selected - expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'synthetics' }); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + dataType: 'synthetics', + time: DEFAULT_TIME, + }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 5c94a5bca60f8..a8f98b98026b6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -9,7 +9,9 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { ReportViewTypeId, SeriesUrl } from '../../types'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; +import { DEFAULT_TIME } from '../../configurations/constants'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; interface Props { reportTypes: Array<{ id: ReportViewTypeId; label: string }>; @@ -21,6 +23,8 @@ export function ReportTypesCol({ reportTypes }: Props) { setSeries, } = useUrlStorage(NEW_SERIES_KEY); + const { indexPattern } = useIndexPatternContext(); + return reportTypes?.length > 0 ? ( {reportTypes.map(({ id: reportType, label }) => ( @@ -31,16 +35,19 @@ export function ReportTypesCol({ reportTypes }: Props) { iconType="arrowRight" color={selectedReportType === reportType ? 'primary' : 'text'} fill={selectedReportType === reportType} + isDisabled={!indexPattern} onClick={() => { if (reportType === selectedReportType) { setSeries(NEW_SERIES_KEY, { dataType: restSeries.dataType, + time: DEFAULT_TIME, } as SeriesUrl); } else { setSeries(NEW_SERIES_KEY, { ...restSeries, reportType, reportDefinitions: {}, + time: restSeries?.time ?? DEFAULT_TIME, }); } }} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx index 6039fd4cba280..4d5033eca241b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; import { ReportDefinition } from '../types'; interface Props { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index 983c18af031d0..2280109fdacdf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -16,7 +16,7 @@ import { ReportTypesCol } from './columns/report_types_col'; import { ReportDefinitionCol } from './columns/report_definition_col'; import { ReportFilters } from './columns/report_filters'; import { ReportBreakdowns } from './columns/report_breakdowns'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; import { getDefaultConfigs } from '../configurations/default_configs'; @@ -49,7 +49,14 @@ export const ReportTypes: Record { @@ -145,7 +154,7 @@ export function SeriesBuilder() { columns={columns} cellProps={{ style: { borderRight: '1px solid #d3dae6' } }} /> - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx index 71e3317ad6db8..960c2978287bc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -8,8 +8,9 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useHasData } from '../../../../hooks/use_has_data'; -import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useUrlStorage } from '../hooks/use_url_storage'; import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; +import { DEFAULT_TIME } from '../configurations/constants'; export interface TimePickerTime { from: string; @@ -38,7 +39,7 @@ export function SeriesDatePicker({ seriesId }: Props) { useEffect(() => { if (!series || !series.time) { - setSeries(seriesId, { ...series, time: { from: 'now-5h', to: 'now' } }); + setSeries(seriesId, { ...series, time: DEFAULT_TIME }); } }, [seriesId, series, setSeries]); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx index acc9ba9658a08..8fe1d5ed9f2ac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { mockUrlStorage, mockUseHasData, render } from '../rtl_helpers'; import { fireEvent, waitFor } from '@testing-library/react'; import { SeriesDatePicker } from './index'; +import { DEFAULT_TIME } from '../configurations/constants'; describe('SeriesDatePicker', function () { it('should render properly', function () { @@ -40,7 +41,7 @@ describe('SeriesDatePicker', function () { expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { breakdown: 'monitor.status', reportType: 'upp', - time: { from: 'now-5h', to: 'now' }, + time: DEFAULT_TIME, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx index c6209381a4da1..fe54262e13844 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { DataSeries } from '../../types'; -import { SeriesChartTypes } from './chart_types'; -import { MetricSelection } from './metric_selection'; +import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; +import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; interface Props { series: DataSeries; @@ -17,13 +17,13 @@ interface Props { export function ActionsCol({ series }: Props) { return ( - + - + - {series.hasMetricType && ( + {series.hasOperationType && ( - + )} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 654a93a08a7c8..0824f13e6b3fe 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; import { mockIndexPattern, mockUrlStorage, render } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_storage'; import { getDefaultConfigs } from '../../configurations/default_configs'; -import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Breakdowns', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 0d34d7245725a..5561779daa8c4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FieldLabels } from '../../configurations/constants'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx deleted file mode 100644 index 017655053eef2..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx +++ /dev/null @@ -1,149 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; - -import { - EuiButton, - EuiButtonGroup, - EuiButtonIcon, - EuiLoadingSpinner, - EuiPopover, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { useFetcher } from '../../../../..'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; -import { SeriesType } from '../../../../../../../lens/public'; - -export function SeriesChartTypes({ - seriesId, - defaultChartType, -}: { - seriesId: string; - defaultChartType: SeriesType; -}) { - const { series, setSeries, allSeries } = useUrlStorage(seriesId); - - const seriesType = series?.seriesType ?? defaultChartType; - - const onChange = (value: SeriesType) => { - Object.keys(allSeries).forEach((seriesKey) => { - const seriesN = allSeries[seriesKey]; - - setSeries(seriesKey, { ...seriesN, seriesType: value }); - }); - }; - - return ( - - ); -} - -export interface XYChartTypesProps { - onChange: (value: SeriesType) => void; - value: SeriesType; - label?: string; - includeChartTypes?: string[]; - excludeChartTypes?: string[]; -} - -export function XYChartTypes({ - onChange, - value, - label, - includeChartTypes, - excludeChartTypes, -}: XYChartTypesProps) { - const [isOpen, setIsOpen] = useState(false); - - const { - services: { lens }, - } = useKibana(); - - const { data = [], loading } = useFetcher(() => lens.getXyVisTypes(), [lens]); - - let vizTypes = data ?? []; - - if ((excludeChartTypes ?? []).length > 0) { - vizTypes = vizTypes.filter(({ id }) => !excludeChartTypes?.includes(id)); - } - - if ((includeChartTypes ?? []).length > 0) { - vizTypes = vizTypes.filter(({ id }) => includeChartTypes?.includes(id)); - } - - return loading ? ( - - ) : ( - id === value)?.icon} - onClick={() => { - setIsOpen((prevState) => !prevState); - }} - > - {label} - - ) : ( - id === value)?.label} - iconType={vizTypes.find(({ id }) => id === value)?.icon!} - onClick={() => { - setIsOpen((prevState) => !prevState); - }} - /> - ) - } - closePopover={() => setIsOpen(false)} - > - ({ - id: t.id, - label: t.label, - title: t.label, - iconType: t.icon || 'empty', - 'data-test-subj': `lnsXY_seriesType-${t.id}`, - }))} - idSelected={value} - onChange={(valueN: string) => { - onChange(valueN as SeriesType); - }} - /> - - ); -} - -const ButtonGroup = styled(EuiButtonGroup)` - &&& { - .euiButtonGroupButton-isSelected { - background-color: #a5a9b1 !important; - } - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index edd5546f13940..530b8dee3a4d2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; import { mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; -import { USER_AGENT_NAME } from '../../configurations/data/elasticsearch_fieldnames'; +import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 280912dd0902f..3e6d7890f4c81 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -14,7 +14,7 @@ import { EuiFilterGroup, } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 7f76c9ea999ee..befbb3b74d6d7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -12,7 +12,7 @@ import { mockUrlStorage, mockUseSeriesFilter, mockUseValuesList, render } from ' import { USER_AGENT_NAME, USER_AGENT_VERSION, -} from '../../configurations/data/elasticsearch_fieldnames'; +} from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterValueButton', function () { it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 42cdfd595e66b..efccb351c2619 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx deleted file mode 100644 index ced04f0a59c8c..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx +++ /dev/null @@ -1,112 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { mockUrlStorage, render } from '../../rtl_helpers'; -import { MetricSelection } from './metric_selection'; - -describe('MetricSelection', function () { - it('should render properly', function () { - render(); - - screen.getByText('Average'); - }); - - it('should display selected value', function () { - mockUrlStorage({ - data: { - 'performance-distribution': { - reportType: 'kpi', - metric: 'median', - time: { from: 'now-15m', to: 'now' }, - }, - }, - }); - - render(); - - screen.getByText('Median'); - }); - - it('should be disabled on disabled state', function () { - render(); - - const btn = screen.getByRole('button'); - - expect(btn.classList).toContain('euiButton-isDisabled'); - }); - - it('should call set series on change', function () { - const { setSeries } = mockUrlStorage({ - data: { - 'performance-distribution': { - reportType: 'kpi', - metric: 'median', - time: { from: 'now-15m', to: 'now' }, - }, - }, - }); - - render(); - - fireEvent.click(screen.getByText('Median')); - - screen.getByText('Chart metric group'); - - fireEvent.click(screen.getByText('95th Percentile')); - - expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', { - metric: '95th', - reportType: 'kpi', - time: { from: 'now-15m', to: 'now' }, - }); - // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times - // This should be one https://github.com/elastic/eui/issues/4629 - expect(setSeries).toHaveBeenCalledTimes(3); - }); - - it('should call set series on change for all series', function () { - const { setSeries } = mockUrlStorage({ - data: { - 'page-views': { - reportType: 'kpi', - metric: 'median', - time: { from: 'now-15m', to: 'now' }, - }, - 'performance-distribution': { - reportType: 'kpi', - metric: 'median', - time: { from: 'now-15m', to: 'now' }, - }, - }, - }); - - render(); - - fireEvent.click(screen.getByText('Median')); - - screen.getByText('Chart metric group'); - - fireEvent.click(screen.getByText('95th Percentile')); - - expect(setSeries).toHaveBeenNthCalledWith(1, 'page-views', { - metric: '95th', - reportType: 'kpi', - time: { from: 'now-15m', to: 'now' }, - }); - - expect(setSeries).toHaveBeenNthCalledWith(2, 'performance-distribution', { - metric: '95th', - reportType: 'kpi', - time: { from: 'now-15m', to: 'now' }, - }); - // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times - // This should be one https://github.com/elastic/eui/issues/4629 - expect(setSeries).toHaveBeenCalledTimes(6); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx deleted file mode 100644 index e01e371b5eeeb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx +++ /dev/null @@ -1,86 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; -import { OperationType } from '../../../../../../../lens/public'; - -const toggleButtons = [ - { - id: `avg`, - label: i18n.translate('xpack.observability.expView.metricsSelect.average', { - defaultMessage: 'Average', - }), - }, - { - id: `median`, - label: i18n.translate('xpack.observability.expView.metricsSelect.median', { - defaultMessage: 'Median', - }), - }, - { - id: `95th`, - label: i18n.translate('xpack.observability.expView.metricsSelect.9thPercentile', { - defaultMessage: '95th Percentile', - }), - }, - { - id: `99th`, - label: i18n.translate('xpack.observability.expView.metricsSelect.99thPercentile', { - defaultMessage: '99th Percentile', - }), - }, -]; - -export function MetricSelection({ - seriesId, - isDisabled, -}: { - seriesId: string; - isDisabled: boolean; -}) { - const { series, setSeries, allSeries } = useUrlStorage(seriesId); - - const [isOpen, setIsOpen] = useState(false); - - const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'avg'); - - const onChange = (optionId: OperationType) => { - setToggleIdSelected(optionId); - - Object.keys(allSeries).forEach((seriesKey) => { - const seriesN = allSeries[seriesKey]; - - setSeries(seriesKey, { ...seriesN, metric: optionId }); - }); - }; - const button = ( - setIsOpen((prevState) => !prevState)} - size="s" - color="text" - isDisabled={isDisabled} - > - {toggleButtons.find(({ id }) => id === toggleIdSelected)!.label} - - ); - - return ( - setIsOpen(false)}> - onChange(id as OperationType)} - /> - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx index 67aebed943326..aaaa02c7c5697 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import { DataSeries } from '../../types'; -import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useUrlStorage } from '../../hooks/use_url_storage'; interface Props { series: DataSeries; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx index 24b65d2adb38e..c9bb44cfd8cca 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -17,9 +17,9 @@ import { } from '@elastic/eui'; import { FilterExpanded } from './filter_expanded'; import { DataSeries } from '../../types'; -import { FieldLabels } from '../../configurations/constants'; +import { FieldLabels } from '../../configurations/constants/constants'; import { SelectedFilters } from '../selected_filters'; -import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage'; interface Props { seriesId: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 5770a7e209f06..a38b50d610c75 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -10,8 +10,8 @@ import { screen, waitFor } from '@testing-library/react'; import { mockIndexPattern, mockUrlStorage, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { NEW_SERIES_KEY } from '../hooks/use_url_strorage'; -import { USER_AGENT_NAME } from '../configurations/data/elasticsearch_fieldnames'; +import { NEW_SERIES_KEY } from '../hooks/use_url_storage'; +import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; describe('SelectedFilters', function () { const dataViewSeries = getDefaultConfigs({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx index be8b1feb4d723..34e69f688eaaf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { FilterLabel } from '../components/filter_label'; import { DataSeries, UrlFilter } from '../types'; import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 2d423c9aee3fc..2d8bd12904fbd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -13,7 +13,7 @@ import { ActionsCol } from './columns/actions_col'; import { Breakdowns } from './columns/breakdowns'; import { DataSeries } from '../types'; import { SeriesBuilder } from '../series_builder/series_builder'; -import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DatePickerCol } from './columns/date_picker_col'; import { RemoveSeries } from './columns/remove_series'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 444e0ddaecb4a..141dcecd0ba5b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -9,9 +9,9 @@ import { PaletteOutput } from 'src/plugins/charts/public'; import { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, + FieldBasedIndexPatternColumn, SeriesType, OperationType, - IndexPatternColumn, } from '../../../../../lens/public'; import { PersistableFilter } from '../../../../../lens/common'; @@ -41,14 +41,19 @@ export interface ReportDefinition { required?: boolean; custom?: boolean; defaultValue?: string; - options?: Array<{ field: string; label: string; description?: string }>; + options?: Array<{ + field: string; + label: string; + description?: string; + columnType?: 'range' | 'operation'; + }>; } export interface DataSeries { reportType: ReportViewType; id: string; xAxisColumn: Partial | Partial; - yAxisColumn: Partial; + yAxisColumn: Partial; breakdowns: string[]; defaultSeriesType: SeriesType; @@ -57,7 +62,7 @@ export interface DataSeries { filters?: PersistableFilter[]; reportDefinitions: ReportDefinition[]; labels: Record; - hasMetricType: boolean; + hasOperationType: boolean; palette?: PaletteOutput; } @@ -70,7 +75,7 @@ export interface SeriesUrl { filters?: UrlFilter[]; seriesType?: SeriesType; reportType: ReportViewTypeId; - metric?: OperationType; + operationType?: OperationType; dataType?: AppDataType; reportDefinitions?: Record; } @@ -87,3 +92,22 @@ export interface ConfigProps { } export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm'; + +type FormatType = 'duration' | 'number'; +type InputFormat = 'microseconds' | 'milliseconds' | 'seconds'; +type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize'; + +export interface FieldFormatParams { + inputFormat: InputFormat; + outputFormat: OutputFormat; + outputPrecision?: number; + showSuffix?: boolean; +} + +export interface FieldFormat { + field: string; + format: { + id: FormatType; + params: FieldFormatParams; + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts new file mode 100644 index 0000000000000..b6f544db2a319 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { indexPatternList, ObservabilityIndexPatterns } from './observability_index_patterns'; +import { mockCore, mockIndexPattern } from '../rtl_helpers'; +import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public'; + +const fieldFormats = { + 'transaction.duration.us': { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asSeconds', + outputPrecision: 1, + showSuffix: true, + }, + }, + 'transaction.experience.fid': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.experience.tbt': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.marks.agent.firstContentfulPaint': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, + 'transaction.marks.agent.largestContentfulPaint': { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + }, +}; + +describe('ObservabilityIndexPatterns', function () { + const { data } = mockCore(); + data!.indexPatterns.get = jest.fn().mockReturnValue({ title: 'index-*' }); + data!.indexPatterns.createAndSave = jest.fn().mockReturnValue({ id: indexPatternList.rum }); + data!.indexPatterns.updateSavedObject = jest.fn(); + + it('should return index pattern for app', async function () { + const obsv = new ObservabilityIndexPatterns(data!); + + const indexP = await obsv.getIndexPattern('rum'); + + expect(indexP).toEqual({ title: 'index-*' }); + + expect(data?.indexPatterns.get).toHaveBeenCalledWith(indexPatternList.rum); + expect(data?.indexPatterns.get).toHaveBeenCalledTimes(1); + }); + + it('should creates missing index pattern', async function () { + data!.indexPatterns.get = jest.fn().mockImplementation(() => { + throw new SavedObjectNotFound('index_pattern'); + }); + + const obsv = new ObservabilityIndexPatterns(data!); + + const indexP = await obsv.getIndexPattern('rum'); + + expect(indexP).toEqual({ id: indexPatternList.rum }); + + expect(data?.indexPatterns.createAndSave).toHaveBeenCalledWith({ + fieldFormats, + id: 'rum_static_index_pattern_id', + timeFieldName: '@timestamp', + title: '(rum-data-view)*,apm-*', + }); + expect(data?.indexPatterns.createAndSave).toHaveBeenCalledTimes(1); + }); + + it('should return getFieldFormats', function () { + const obsv = new ObservabilityIndexPatterns(data!); + + expect(obsv.getFieldFormats('rum')).toEqual(fieldFormats); + }); + + it('should validate field formats', async function () { + mockIndexPattern.getFormatterForField = jest.fn().mockReturnValue({ params: () => {} }); + + const obsv = new ObservabilityIndexPatterns(data!); + + await obsv.validateFieldFormats('rum', mockIndexPattern); + + expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledTimes(1); + expect(data?.indexPatterns.updateSavedObject).toHaveBeenCalledWith( + expect.objectContaining({ fieldFormatMap: fieldFormats }) + ); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts new file mode 100644 index 0000000000000..527ef48364d22 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { + DataPublicPluginStart, + IndexPattern, + FieldFormat as IFieldFormat, + IndexPatternSpec, +} from '../../../../../../../../src/plugins/data/public'; +import { rumFieldFormats } from '../configurations/rum/field_formats'; +import { syntheticsFieldFormats } from '../configurations/synthetics/field_formats'; +import { FieldFormat, FieldFormatParams } from '../types'; + +const appFieldFormats: Record = { + rum: rumFieldFormats, + apm: null, + logs: null, + metrics: null, + synthetics: syntheticsFieldFormats, +}; + +function getFieldFormatsForApp(app: DataType) { + return appFieldFormats[app]; +} + +export type DataType = 'synthetics' | 'apm' | 'logs' | 'metrics' | 'rum'; + +export const indexPatternList: Record = { + synthetics: 'synthetics_static_index_pattern_id', + apm: 'apm_static_index_pattern_id', + rum: 'rum_static_index_pattern_id', + logs: 'logs_static_index_pattern_id', + metrics: 'metrics_static_index_pattern_id', +}; + +const appToPatternMap: Record = { + synthetics: '(synthetics-data-view)*,heartbeat-*,synthetics-*', + apm: 'apm-*', + rum: '(rum-data-view)*,apm-*', + logs: 'logs-*,filebeat-*', + metrics: 'metrics-*,metricbeat-*', +}; + +export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldFormatParams) { + const isSame = + param1?.inputFormat === param2?.inputFormat && + param1?.outputFormat === param2?.outputFormat && + param1?.showSuffix === param2?.showSuffix; + + if (param2.outputPrecision !== undefined) { + return param2?.outputPrecision === param1?.outputPrecision && isSame; + } + + return isSame; +} + +export class ObservabilityIndexPatterns { + data?: DataPublicPluginStart; + + constructor(data: DataPublicPluginStart) { + this.data = data; + } + + async createIndexPattern(app: DataType) { + if (!this.data) { + throw new Error('data is not defined'); + } + + const pattern = appToPatternMap[app]; + + return await this.data.indexPatterns.createAndSave({ + title: pattern, + id: indexPatternList[app], + timeFieldName: '@timestamp', + fieldFormats: this.getFieldFormats(app), + }); + } + // we want to make sure field formats remain same + async validateFieldFormats(app: DataType, indexPattern: IndexPattern) { + const defaultFieldFormats = getFieldFormatsForApp(app); + if (defaultFieldFormats && defaultFieldFormats.length > 0) { + let isParamsDifferent = false; + defaultFieldFormats.forEach(({ field, format }) => { + const fieldFormat = indexPattern.getFormatterForField(indexPattern.getFieldByName(field)!); + const params = fieldFormat.params(); + if (!isParamsSame(params, format.params)) { + indexPattern.setFieldFormat(field, format); + isParamsDifferent = true; + } + }); + if (isParamsDifferent) { + await this.data?.indexPatterns.updateSavedObject(indexPattern); + } + } + } + + getFieldFormats(app: DataType) { + const fieldFormatMap: IndexPatternSpec['fieldFormats'] = {}; + + (appFieldFormats?.[app] ?? []).forEach(({ field, format }) => { + fieldFormatMap[field] = format; + }); + + return fieldFormatMap; + } + + async getIndexPattern(app: DataType): Promise { + if (!this.data) { + throw new Error('data is not defined'); + } + try { + const indexPattern = await this.data?.indexPatterns.get(indexPatternList[app]); + + // this is intentional a non blocking call, so no await clause + this.validateFieldFormats(app, indexPattern); + return indexPattern; + } catch (e: unknown) { + if (e instanceof SavedObjectNotFound) { + return await this.createIndexPattern(app || 'apm'); + } + } + } +} diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index a44aab2da85be..d14039ba173ac 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -76,6 +76,7 @@ export function FieldValueSelection({ ; + export interface EventHit extends SearchHit { sort: string[]; _source: EventSource; - fields: Record; + fields: Fields; aggregations: { // eslint-disable-next-line @typescript-eslint/no-explicit-any [agg: string]: any; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts similarity index 97% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts rename to x-pack/plugins/security_solution/common/utils/field_formatters.test.ts index dc3efc6909c63..b724c0f672b50 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts +++ b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { EventHit, EventSource } from '../../../../../../common/search_strategy'; -import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; -import { eventDetailsFormattedFields, eventHit } from '../mocks'; +import { EventHit, EventSource } from '../search_strategy'; +import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters'; +import { eventDetailsFormattedFields, eventHit } from './mock_event_details'; describe('Events Details Helpers', () => { const fields: EventHit['fields'] = eventHit.fields; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.ts similarity index 96% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts rename to x-pack/plugins/security_solution/common/utils/field_formatters.ts index 2fc729729e435..b436f8e616122 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/common/utils/field_formatters.ts @@ -7,12 +7,8 @@ import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; -import { - EventHit, - EventSource, - TimelineEventsDetailsItem, -} from '../../../../../../common/search_strategy'; -import { toObjectArrayOfStrings, toStringArray } from '../../../../helpers/to_array'; +import { EventHit, EventSource, TimelineEventsDetailsItem } from '../search_strategy'; +import { toObjectArrayOfStrings, toStringArray } from './to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts b/x-pack/plugins/security_solution/common/utils/mock_event_details.ts similarity index 97% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts rename to x-pack/plugins/security_solution/common/utils/mock_event_details.ts index 13b7fe7051246..7dc257ebb3fef 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts +++ b/x-pack/plugins/security_solution/common/utils/mock_event_details.ts @@ -40,7 +40,7 @@ export const eventHit = { 'source.geo.location': [{ coordinates: [118.7778, 32.0617], type: 'Point' }], 'threat.indicator': [ { - 'matched.field': ['matched_field'], + 'matched.field': ['matched_field', 'other_matched_field'], first_seen: ['2021-02-22T17:29:25.195Z'], provider: ['yourself'], type: ['custom'], @@ -259,8 +259,8 @@ export const eventDetailsFormattedFields = [ { category: 'threat', field: 'threat.indicator.matched.field', - values: ['matched_field', 'matched_field_2'], - originalValue: ['matched_field', 'matched_field_2'], + values: ['matched_field', 'other_matched_field', 'matched_field_2'], + originalValue: ['matched_field', 'other_matched_field', 'matched_field_2'], isObjectArray: false, }, { diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/common/utils/to_array.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts rename to x-pack/plugins/security_solution/common/utils/to_array.ts diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts index ba0567c40eb92..3edd6e6fda14b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts @@ -655,4 +655,16 @@ export const mockAlertDetailsData = [ values: ['7.10.0'], originalValue: ['7.10.0'], }, + { + category: 'threat', + field: 'threat.indicator', + values: [`{"first_seen":"2021-03-25T18:17:00.000Z"}`], + originalValue: [`{"first_seen":"2021-03-25T18:17:00.000Z"}`], + }, + { + category: 'threat', + field: 'threat.indicator.matched', + values: `["file", "url"]`, + originalValue: ['file', 'url'], + }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index c19a3952220cf..b8f29996d603b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; -import { SummaryViewComponent } from './summary_view'; +import { AlertSummaryView } from './alert_summary_view'; import { mockAlertDetailsData } from './__mocks__'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; @@ -30,7 +30,7 @@ const props = { timelineId: 'detections-page', }; -describe('SummaryViewComponent', () => { +describe('AlertSummaryView', () => { const mount = useMountAppended(); beforeEach(() => { @@ -44,7 +44,7 @@ describe('SummaryViewComponent', () => { test('render correct items', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="summary-view"]').exists()).toEqual(true); @@ -53,7 +53,7 @@ describe('SummaryViewComponent', () => { test('render investigation guide', async () => { const wrapper = mount( - + ); await waitFor(() => { @@ -69,7 +69,7 @@ describe('SummaryViewComponent', () => { }); const wrapper = mount( - + ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx new file mode 100644 index 0000000000000..091049b967f02 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiBasicTableColumn, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, +} from '@elastic/eui'; +import { get, getOr } from 'lodash/fp'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { + ALERTS_HEADERS_RISK_SCORE, + ALERTS_HEADERS_RULE, + ALERTS_HEADERS_SEVERITY, + ALERTS_HEADERS_THRESHOLD_CARDINALITY, + ALERTS_HEADERS_THRESHOLD_COUNT, + ALERTS_HEADERS_THRESHOLD_TERMS, +} from '../../../detections/components/alerts_table/translations'; +import { + IP_FIELD_TYPE, + SIGNAL_RULE_NAME_FIELD_NAME, +} from '../../../timelines/components/timeline/body/renderers/constants'; +import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip'; +import { SummaryView } from './summary_view'; +import { AlertSummaryRow, getSummaryColumns, SummaryRow } from './helpers'; +import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; +import * as i18n from './translations'; +import { LineClamp } from '../line_clamp'; + +const StyledEuiDescriptionList = styled(EuiDescriptionList)` + padding: 24px 4px 4px; +`; + +const fields = [ + { id: 'signal.status' }, + { id: '@timestamp' }, + { + id: SIGNAL_RULE_NAME_FIELD_NAME, + linkField: 'signal.rule.id', + label: ALERTS_HEADERS_RULE, + }, + { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, + { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, + { id: 'host.name' }, + { id: 'user.name' }, + { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, + { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, + { id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT }, + { id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS }, + { id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY }, +]; + +const getDescription = ({ + contextId, + eventId, + fieldName, + value, + fieldType = '', + linkValue, +}: AlertSummaryRow['description']) => ( + +); + +const getSummaryRows = ({ + data, + browserFields, + timelineId, + eventId, +}: { + data: TimelineEventsDetailsItem[]; + browserFields: BrowserFields; + timelineId: string; + eventId: string; +}) => { + return data != null + ? fields.reduce((acc, item) => { + const field = data.find((d) => d.field === item.id); + if (!field) { + return acc; + } + const linkValueField = + item.linkField != null && data.find((d) => d.field === item.linkField); + const linkValue = getOr(null, 'originalValue.0', linkValueField); + const value = getOr(null, 'originalValue.0', field); + const category = field.category; + const fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string; + const description = { + contextId: timelineId, + eventId, + fieldName: item.id, + value, + fieldType: item.fieldType ?? fieldType, + linkValue: linkValue ?? undefined, + }; + + if (item.id === 'signal.threshold_result.terms') { + try { + const terms = getOr(null, 'originalValue', field); + const parsedValue = terms.map((term: string) => JSON.parse(term)); + const thresholdTerms = (parsedValue ?? []).map( + (entry: { field: string; value: string }) => { + return { + title: `${entry.field} [threshold]`, + description: { + ...description, + value: entry.value, + }, + }; + } + ); + return [...acc, ...thresholdTerms]; + } catch (err) { + return acc; + } + } + + if (item.id === 'signal.threshold_result.cardinality') { + try { + const parsedValue = JSON.parse(value); + return [ + ...acc, + { + title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, + description: { + ...description, + value: `count(${parsedValue.field}) == ${parsedValue.value}`, + }, + }, + ]; + } catch (err) { + return acc; + } + } + + return [ + ...acc, + { + title: item.label ?? item.id, + description, + }, + ]; + }, []) + : []; +}; + +const summaryColumns: Array> = getSummaryColumns(getDescription); + +const AlertSummaryViewComponent: React.FC<{ + browserFields: BrowserFields; + data: TimelineEventsDetailsItem[]; + eventId: string; + timelineId: string; +}> = ({ browserFields, data, eventId, timelineId }) => { + const summaryRows = useMemo(() => getSummaryRows({ browserFields, data, eventId, timelineId }), [ + browserFields, + data, + eventId, + timelineId, + ]); + + const ruleId = useMemo(() => { + const item = data.find((d) => d.field === 'signal.rule.id'); + return Array.isArray(item?.originalValue) + ? item?.originalValue[0] + : item?.originalValue ?? null; + }, [data]); + const { rule: maybeRule } = useRuleAsync(ruleId); + + return ( + <> + + {maybeRule?.note && ( + + {i18n.INVESTIGATION_GUIDE} + + + + + )} + + ); +}; + +export const AlertSummaryView = React.memo(AlertSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 3fe8f8a147ea8..197022ce316c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -12,7 +12,7 @@ import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; import { mockDetailItemData, mockDetailItemDataId, TestProviders } from '../../mock'; -import { EventDetails, EventsViewType } from './event_details'; +import { EventDetails, EventsViewType, EventView, ThreatView } from './event_details'; import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; import { mockAlertDetailsData } from './__mocks__'; @@ -28,10 +28,12 @@ describe('EventDetails', () => { data: mockDetailItemData, id: mockDetailItemDataId, isAlert: false, - onViewSelected: jest.fn(), + onEventViewSelected: jest.fn(), + onThreatViewSelected: jest.fn(), timelineTabType: TimelineTabs.query, timelineId: 'test', - view: EventsViewType.summaryView, + eventView: EventsViewType.summaryView as EventView, + threatView: EventsViewType.threatSummaryView as ThreatView, }; const alertsProps = { @@ -97,4 +99,27 @@ describe('EventDetails', () => { ).toEqual('Summary'); }); }); + + describe('threat tabs', () => { + ['Threat Summary', 'Threat Details'].forEach((tab) => { + test(`it renders the ${tab} tab`, () => { + expect( + alertsWrapper + .find('[data-test-subj="threatDetails"]') + .find('[role="tablist"]') + .containsMatchingElement({tab}) + ).toBeTruthy(); + }); + }); + + test('the Summary tab is selected by default', () => { + expect( + alertsWrapper + .find('[data-test-subj="threatDetails"]') + .find('.euiTab-isSelected') + .first() + .text() + ).toEqual('Threat Summary'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 4979d70ce2d7b..0e4cf7f4ae2fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -14,14 +14,23 @@ import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/ti import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; -import { SummaryView } from './summary_view'; +import { AlertSummaryView } from './alert_summary_view'; +import { ThreatSummaryView } from './threat_summary_view'; +import { ThreatDetailsView } from './threat_details_view'; import { TimelineTabs } from '../../../../common/types/timeline'; +import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; -export type View = EventsViewType.tableView | EventsViewType.jsonView | EventsViewType.summaryView; +export type EventView = + | EventsViewType.tableView + | EventsViewType.jsonView + | EventsViewType.summaryView; +export type ThreatView = EventsViewType.threatSummaryView | EventsViewType.threatDetailsView; export enum EventsViewType { tableView = 'table-view', jsonView = 'json-view', summaryView = 'summary-view', + threatSummaryView = 'threat-summary-view', + threatDetailsView = 'threat-details-view', } interface Props { @@ -29,8 +38,10 @@ interface Props { data: TimelineEventsDetailsItem[]; id: string; isAlert: boolean; - view: EventsViewType; - onViewSelected: (selected: EventsViewType) => void; + eventView: EventView; + threatView: ThreatView; + onEventViewSelected: (selected: EventView) => void; + onThreatViewSelected: (selected: ThreatView) => void; timelineTabType: TimelineTabs | 'flyout'; timelineId: string; } @@ -45,7 +56,16 @@ const StyledEuiTabbedContent = styled(EuiTabbedContent)` display: flex; flex: 1; flex-direction: column; - overflow: hidden; + overflow: scroll; + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); + } } `; @@ -57,14 +77,19 @@ const TabContentWrapper = styled.div` const EventDetailsComponent: React.FC = ({ browserFields, data, + eventView, id, - view, - onViewSelected, - timelineTabType, - timelineId, isAlert, + onEventViewSelected, + onThreatViewSelected, + threatView, + timelineId, + timelineTabType, }) => { - const handleTabClick = useCallback((e) => onViewSelected(e.id), [onViewSelected]); + const handleEventTabClick = useCallback((e) => onEventViewSelected(e.id), [onEventViewSelected]); + const handleThreatTabClick = useCallback((e) => onThreatViewSelected(e.id), [ + onThreatViewSelected, + ]); const alerts = useMemo( () => [ @@ -74,11 +99,13 @@ const EventDetailsComponent: React.FC = ({ content: ( <> - ), @@ -122,15 +149,60 @@ const EventDetailsComponent: React.FC = ({ [alerts, browserFields, data, id, isAlert, timelineId, timelineTabType] ); - const selectedTab = useMemo(() => tabs.find((t) => t.id === view) ?? tabs[0], [tabs, view]); + const selectedEventTab = useMemo(() => tabs.find((t) => t.id === eventView) ?? tabs[0], [ + tabs, + eventView, + ]); + + const isThreatPresent: boolean = useMemo( + () => + selectedEventTab.id === tabs[0].id && + isAlert && + data.some((item) => item.field === INDICATOR_DESTINATION_PATH), + [tabs, selectedEventTab, isAlert, data] + ); + + const threatTabs: EuiTabbedContentTab[] = useMemo(() => { + return isAlert && isThreatPresent + ? [ + { + id: EventsViewType.threatSummaryView, + name: i18n.THREAT_SUMMARY, + content: , + }, + { + id: EventsViewType.threatDetailsView, + name: i18n.THREAT_DETAILS, + content: , + }, + ] + : []; + }, [data, id, isAlert, timelineId, isThreatPresent]); + + const selectedThreatTab = useMemo( + () => threatTabs.find((t) => t.id === threatView) ?? threatTabs[0], + [threatTabs, threatView] + ); return ( - + <> + + {isThreatPresent && ( + + )} + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 00e2ee276f181..67e67584849cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -7,6 +7,8 @@ import { get, getOr, isEmpty, uniqBy } from 'lodash/fp'; +import React from 'react'; +import { EuiBasicTableColumn, EuiTitle } from '@elastic/eui'; import { elementOrChildrenHasFocus, getFocusedDataColindexCell, @@ -51,6 +53,38 @@ export interface Item { values: ToStringArray; } +export interface AlertSummaryRow { + title: string; + description: { + contextId: string; + eventId: string; + fieldName: string; + value: string; + fieldType: string; + linkValue: string | undefined; + }; +} + +export interface ThreatSummaryRow { + title: string; + description: { + contextId: string; + eventId: string; + fieldName: string; + values: string[]; + }; +} + +export interface ThreatDetailsRow { + title: string; + description: { + fieldName: string; + value: string; + }; +} + +export type SummaryRow = AlertSummaryRow | ThreatSummaryRow | ThreatDetailsRow; + export const getColumnHeaderFromBrowserField = ({ browserField, width = DEFAULT_COLUMN_MIN_WIDTH, @@ -172,3 +206,33 @@ export const onEventDetailsTabKeyPressed = ({ }); } }; + +const getTitle = (title: string) => ( + +
{title}
+
+); +getTitle.displayName = 'getTitle'; + +export const getSummaryColumns = ( + DescriptionComponent: + | React.FC + | React.FC + | React.FC +): Array> => { + return [ + { + field: 'title', + truncateText: false, + render: getTitle, + width: '120px', + name: '', + }, + { + field: 'description', + truncateText: false, + render: DescriptionComponent, + name: '', + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 8e07910c1c071..3b2c55e9a6b67 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -5,69 +5,11 @@ * 2.0. */ -import { get, getOr } from 'lodash/fp'; -import { - EuiTitle, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, - EuiInMemoryTable, - EuiBasicTableColumn, -} from '@elastic/eui'; -import React, { useMemo } from 'react'; +import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import React from 'react'; import styled from 'styled-components'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; -import * as i18n from './translations'; -import { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { - ALERTS_HEADERS_RISK_SCORE, - ALERTS_HEADERS_RULE, - ALERTS_HEADERS_SEVERITY, - ALERTS_HEADERS_THRESHOLD_COUNT, - ALERTS_HEADERS_THRESHOLD_TERMS, - ALERTS_HEADERS_THRESHOLD_CARDINALITY, -} from '../../../detections/components/alerts_table/translations'; -import { - IP_FIELD_TYPE, - SIGNAL_RULE_NAME_FIELD_NAME, -} from '../../../timelines/components/timeline/body/renderers/constants'; -import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip'; -import { LineClamp } from '../line_clamp'; -import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; - -interface SummaryRow { - title: string; - description: { - contextId: string; - eventId: string; - fieldName: string; - value: string; - fieldType: string; - linkValue: string | undefined; - }; -} -type Summary = SummaryRow[]; - -const fields = [ - { id: 'signal.status' }, - { id: '@timestamp' }, - { - id: SIGNAL_RULE_NAME_FIELD_NAME, - linkField: 'signal.rule.id', - label: ALERTS_HEADERS_RULE, - }, - { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, - { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, - { id: 'host.name' }, - { id: 'user.name' }, - { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, - { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, - { id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT }, - { id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS }, - { id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY }, -]; +import { SummaryRow } from './helpers'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` @@ -77,173 +19,26 @@ const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` .euiTableRowCell { border: none; } -`; -const StyledEuiDescriptionList = styled(EuiDescriptionList)` - padding: 24px 4px 4px; + .euiTableCellContent { + display: flex; + flex-direction: column; + align-items: flex-start; + } `; -const getTitle = (title: SummaryRow['title']) => ( - -
{title}
-
-); - -getTitle.displayName = 'getTitle'; - -const getDescription = ({ - contextId, - eventId, - fieldName, - value, - fieldType = '', - linkValue, -}: SummaryRow['description']) => ( - -); - -const getSummary = ({ - data, - browserFields, - timelineId, - eventId, -}: { - data: TimelineEventsDetailsItem[]; - browserFields: BrowserFields; - timelineId: string; - eventId: string; -}) => { - return data != null - ? fields.reduce((acc, item) => { - const field = data.find((d) => d.field === item.id); - if (!field) { - return acc; - } - const linkValueField = - item.linkField != null && data.find((d) => d.field === item.linkField); - const linkValue = getOr(null, 'originalValue.0', linkValueField); - const value = getOr(null, 'originalValue.0', field); - const category = field.category; - const fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string; - const description = { - contextId: timelineId, - eventId, - fieldName: item.id, - value, - fieldType: item.fieldType ?? fieldType, - linkValue: linkValue ?? undefined, - }; - - if (item.id === 'signal.threshold_result.terms') { - try { - const terms = getOr(null, 'originalValue', field); - const parsedValue = terms.map((term: string) => JSON.parse(term)); - const thresholdTerms = (parsedValue ?? []).map( - (entry: { field: string; value: string }) => { - return { - title: `${entry.field} [threshold]`, - description: { - ...description, - value: entry.value, - }, - }; - } - ); - return [...acc, ...thresholdTerms]; - } catch (err) { - return acc; - } - } - - if (item.id === 'signal.threshold_result.cardinality') { - try { - const parsedValue = JSON.parse(value); - return [ - ...acc, - { - title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, - description: { - ...description, - value: `count(${parsedValue.field}) == ${parsedValue.value}`, - }, - }, - ]; - } catch (err) { - return acc; - } - } - - return [ - ...acc, - { - title: item.label ?? item.id, - description, - }, - ]; - }, []) - : []; -}; - -const summaryColumns: Array> = [ - { - field: 'title', - truncateText: false, - render: getTitle, - width: '120px', - name: '', - }, - { - field: 'description', - truncateText: false, - render: getDescription, - name: '', - }, -]; - export const SummaryViewComponent: React.FC<{ - browserFields: BrowserFields; - data: TimelineEventsDetailsItem[]; - eventId: string; - timelineId: string; -}> = ({ data, eventId, timelineId, browserFields }) => { - const ruleId = useMemo(() => { - const item = data.find((d) => d.field === 'signal.rule.id'); - return Array.isArray(item?.originalValue) - ? item?.originalValue[0] - : item?.originalValue ?? null; - }, [data]); - const { rule: maybeRule } = useRuleAsync(ruleId); - const summaryList = useMemo(() => getSummary({ browserFields, data, eventId, timelineId }), [ - browserFields, - data, - eventId, - timelineId, - ]); - + summaryColumns: Array>; + summaryRows: SummaryRow[]; + dataTestSubj?: string; +}> = ({ summaryColumns, summaryRows, dataTestSubj = 'summary-view' }) => { return ( - <> - - {maybeRule?.note && ( - - {i18n.INVESTIGATION_GUIDE} - - - - - )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx new file mode 100644 index 0000000000000..81bffe9b66638 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { ThreatDetailsView } from './threat_details_view'; +import { mockAlertDetailsData } from './__mocks__'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; + +import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { + return { + useRuleAsync: jest.fn(), + }; +}); + +const props = { + data: mockAlertDetailsData as TimelineEventsDetailsItem[], + eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', + timelineId: 'detections-page', +}; + +describe('ThreatDetailsView', () => { + const mount = useMountAppended(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('render correct items', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="threat-details-view-0"]').exists()).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx new file mode 100644 index 0000000000000..0889986237442 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiToolTip, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { SummaryView } from './summary_view'; +import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from './helpers'; +import { getDataFromSourceHits } from '../../../../common/utils/field_formatters'; +import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; + +const ThreatDetailsDescription: React.FC = ({ + fieldName, + value, +}) => ( + + + {fieldName} + + + } + > + {value} + +); + +const getSummaryRowsArray = ({ + data, +}: { + data: TimelineEventsDetailsItem[]; +}): ThreatDetailsRow[][] => { + if (!data) return [[]]; + const threatInfo = data.find( + ({ field, originalValue }) => field === INDICATOR_DESTINATION_PATH && originalValue + ); + if (!threatInfo) return [[]]; + const { originalValue } = threatInfo; + const values = Array.isArray(originalValue) ? originalValue : [originalValue]; + return values.map((value) => + getDataFromSourceHits(JSON.parse(value)).map((threatInfoItem) => ({ + title: threatInfoItem.field.replace(`${INDICATOR_DESTINATION_PATH}.`, ''), + description: { fieldName: threatInfoItem.field, value: threatInfoItem.originalValue }, + })) + ); +}; + +const summaryColumns: Array> = getSummaryColumns( + ThreatDetailsDescription +); + +const ThreatDetailsViewComponent: React.FC<{ + data: TimelineEventsDetailsItem[]; +}> = ({ data }) => { + const summaryRowsArray = useMemo(() => getSummaryRowsArray({ data }), [data]); + return ( + <> + {summaryRowsArray.map((summaryRows, index, arr) => { + const key = summaryRows.find((threat) => threat.title === 'matched.id')?.description + .value[0]; + return ( +
+ + {index < arr.length - 1 && } +
+ ); + })} + + ); +}; + +export const ThreatDetailsView = React.memo(ThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx new file mode 100644 index 0000000000000..756fc7d32b371 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { ThreatSummaryView } from './threat_summary_view'; +import { mockAlertDetailsData } from './__mocks__'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; + +import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { + return { + useRuleAsync: jest.fn(), + }; +}); + +const props = { + data: mockAlertDetailsData as TimelineEventsDetailsItem[], + eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', + timelineId: 'detections-page', +}; + +describe('ThreatSummaryView', () => { + const mount = useMountAppended(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('render correct items', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="threat-summary-view"]').exists()).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx new file mode 100644 index 0000000000000..96ae2071c449b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBasicTableColumn } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; +import { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { SummaryView } from './summary_view'; +import { getSummaryColumns, SummaryRow, ThreatSummaryRow } from './helpers'; +import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; + +const getDescription = ({ + contextId, + eventId, + fieldName, + values, +}: ThreatSummaryRow['description']): JSX.Element => ( + <> + {values.map((value: string) => ( + + ))} + +); + +const getSummaryRows = ({ + data, + timelineId: contextId, + eventId, +}: { + data: TimelineEventsDetailsItem[]; + browserFields?: BrowserFields; + timelineId: string; + eventId: string; +}) => { + if (!data) return []; + return data.reduce((acc, { field, originalValue }) => { + if (field.startsWith(`${INDICATOR_DESTINATION_PATH}.`) && originalValue) { + return [ + ...acc, + { + title: field.replace(`${INDICATOR_DESTINATION_PATH}.`, ''), + description: { + values: Array.isArray(originalValue) ? originalValue : [originalValue], + contextId, + eventId, + fieldName: field, + }, + }, + ]; + } + return acc; + }, []); +}; + +const summaryColumns: Array> = getSummaryColumns(getDescription); + +const ThreatSummaryViewComponent: React.FC<{ + data: TimelineEventsDetailsItem[]; + eventId: string; + timelineId: string; +}> = ({ data, eventId, timelineId }) => { + const summaryRows = useMemo(() => getSummaryRows({ data, eventId, timelineId }), [ + data, + eventId, + timelineId, + ]); + + return ( + + ); +}; + +export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index 3a599b174251a..73a2e0d57307c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -11,6 +11,14 @@ export const SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.summa defaultMessage: 'Summary', }); +export const THREAT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.threatSummary', { + defaultMessage: 'Threat Summary', +}); + +export const THREAT_DETAILS = i18n.translate('xpack.securitySolution.alertDetails.threatDetails', { + defaultMessage: 'Threat Details', +}); + export const INVESTIGATION_GUIDE = i18n.translate( 'xpack.securitySolution.alertDetails.summary.investigationGuide', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index 6f83c075f0a9a..4ca2980dc74e5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -17,17 +17,19 @@ describe('AlertsUtilityBar', () => { test('renders correctly', () => { const wrapper = shallow( ); @@ -41,17 +43,19 @@ describe('AlertsUtilityBar', () => { const wrapper = mount( @@ -72,22 +76,61 @@ describe('AlertsUtilityBar', () => { ).toEqual(false); }); + test('does not show the showOnlyThreatIndicatorAlerts checked if the showThreatMatchOnly is false', () => { + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be false + expect( + wrapper + .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + }); + test('does show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is true', () => { const onShowBuildingBlockAlertsChanged = jest.fn(); const wrapper = mount( @@ -108,22 +151,61 @@ describe('AlertsUtilityBar', () => { ).toEqual(true); }); + test('does show the showOnlyThreatIndicatorAlerts checked if the showOnlyThreatIndicatorAlerts is true', () => { + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true + expect( + wrapper + .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + test('calls the onShowBuildingBlockAlertsChanged when the check box is clicked', () => { const onShowBuildingBlockAlertsChanged = jest.fn(); const wrapper = mount( @@ -145,21 +227,62 @@ describe('AlertsUtilityBar', () => { expect(onShowBuildingBlockAlertsChanged).toHaveBeenCalled(); }); + test('calls the onShowOnlyThreatIndicatorAlertsChanged when the check box is clicked', () => { + const onShowOnlyThreatIndicatorAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // check the box + wrapper + .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') + .first() + .simulate('change', { target: { checked: true } }); + + // Make sure our callback is called + expect(onShowOnlyThreatIndicatorAlertsChanged).toHaveBeenCalled(); + }); + test('can update showBuildingBlockAlerts from false to true', () => { const Proxy = (props: AlertsUtilityBarProps) => ( @@ -167,17 +290,19 @@ describe('AlertsUtilityBar', () => { const wrapper = mount( ); @@ -214,5 +339,79 @@ describe('AlertsUtilityBar', () => { .prop('checked') ).toEqual(true); }); + + test('can update showOnlyThreatIndicatorAlerts from false to true', () => { + const Proxy = (props: AlertsUtilityBarProps) => ( + + + + ); + + const wrapper = mount( + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should false now since we initially set the showBuildingBlockAlerts to false + expect( + wrapper + .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + + wrapper.setProps({ showOnlyThreatIndicatorAlerts: true }); + wrapper.update(); + + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true now since we changed the showBuildingBlockAlerts from false to true + expect( + wrapper + .find('[data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index ec2f84ba3e12d..bda8c85ddb315 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -30,16 +30,18 @@ import { UpdateAlertsStatus } from '../types'; import { FILTER_CLOSED, FILTER_IN_PROGRESS, FILTER_OPEN } from '../alerts_filter_group'; export interface AlertsUtilityBarProps { - hasIndexWrite: boolean; - hasIndexMaintenance: boolean; areEventsLoading: boolean; clearSelection: () => void; currentFilter: Status; + hasIndexMaintenance: boolean; + hasIndexWrite: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; + onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void; selectAll: () => void; selectedEventIds: Readonly>; showBuildingBlockAlerts: boolean; - onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; showClearSelection: boolean; + showOnlyThreatIndicatorAlerts: boolean; totalCount: number; updateAlertsStatus: UpdateAlertsStatus; } @@ -56,21 +58,22 @@ const BuildingBlockContainer = styled(EuiFlexItem)` rgba(245, 167, 0, 0.05) 2px, rgba(245, 167, 0, 0.05) 10px ); - padding: ${({ theme }) => `${theme.eui.paddingSizes.xs}`}; `; const AlertsUtilityBarComponent: React.FC = ({ - hasIndexWrite, - hasIndexMaintenance, areEventsLoading, clearSelection, - totalCount, - selectedEventIds, currentFilter, + hasIndexMaintenance, + hasIndexWrite, + onShowBuildingBlockAlertsChanged, + onShowOnlyThreatIndicatorAlertsChanged, selectAll, + selectedEventIds, showBuildingBlockAlerts, - onShowBuildingBlockAlertsChanged, showClearSelection, + showOnlyThreatIndicatorAlerts, + totalCount, updateAlertsStatus, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); @@ -144,7 +147,7 @@ const AlertsUtilityBarComponent: React.FC = ({ ); const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( - + = ({ label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} /> + + ) => { + closePopover(); + onShowOnlyThreatIndicatorAlertsChanged(e.target.checked); + }} + checked={showOnlyThreatIndicatorAlerts} + color="text" + data-test-subj="showOnlyThreatIndicatorAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS} + /> + ); @@ -240,5 +257,7 @@ export const AlertsUtilityBar = React.memo( prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.totalCount === nextProps.totalCount && prevProps.showClearSelection === nextProps.showClearSelection && - prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts + prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts && + prevProps.onShowOnlyThreatIndicatorAlertsChanged === + nextProps.onShowOnlyThreatIndicatorAlertsChanged ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts index 9307e8b1cd5f7..c52e443c50753 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts @@ -42,6 +42,13 @@ export const ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK = i18n.translate( } ); +export const ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showOnlyThreatIndicatorAlerts', + { + defaultMessage: 'Show only threat indicator alerts', + } +); + export const CLEAR_SELECTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx index 26bc8f213ca46..79c2a45273c33 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx @@ -6,7 +6,7 @@ */ import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { buildAlertsRuleIdFilter } from './default_config'; +import { buildAlertsRuleIdFilter, buildThreatMatchFilter } from './default_config'; jest.mock('./actions'); @@ -34,7 +34,34 @@ describe('alerts default_config', () => { expect(filters).toHaveLength(1); expect(filters[0]).toEqual(expectedFilter); }); + + describe('buildThreatMatchFilter', () => { + test('given a showOnlyThreatIndicatorAlerts=true this will return an array with a single filter', () => { + const filters: Filter[] = buildThreatMatchFilter(true); + const expectedFilter: Filter = { + meta: { + alias: null, + disabled: false, + negate: false, + key: 'signal.rule.threat_mapping', + type: 'exists', + value: 'exists', + }, + // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] + exists: { + field: 'signal.rule.threat_mapping', + }, + }; + expect(filters).toHaveLength(1); + expect(filters[0]).toEqual(expectedFilter); + }); + test('given a showOnlyThreatIndicatorAlerts=false this will return an empty filter', () => { + const filters: Filter[] = buildThreatMatchFilter(false); + expect(filters).toHaveLength(0); + }); + }); }); + // TODO: move these tests to ../timelines/components/timeline/body/events/event_column_view.tsx // describe.skip('getAlertActions', () => { // let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 4fae2e69ac1f6..6a83039bf1ec8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -39,28 +39,31 @@ export const buildAlertStatusFilter = (status: Status): Filter[] => [ }, ]; -export const buildAlertsRuleIdFilter = (ruleId: string): Filter[] => [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'signal.rule.id', - params: { - query: ruleId, - }, - }, - query: { - match_phrase: { - 'signal.rule.id': ruleId, - }, - }, - }, -]; +export const buildAlertsRuleIdFilter = (ruleId: string | null): Filter[] => + ruleId + ? [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.rule.id', + params: { + query: ruleId, + }, + }, + query: { + match_phrase: { + 'signal.rule.id': ruleId, + }, + }, + }, + ] + : []; -export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): Filter[] => [ - ...(showBuildingBlockAlerts +export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): Filter[] => + showBuildingBlockAlerts ? [] : [ { @@ -75,8 +78,25 @@ export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] exists: { field: 'signal.rule.building_block_type' }, }, - ]), -]; + ]; + +export const buildThreatMatchFilter = (showOnlyThreatIndicatorAlerts: boolean): Filter[] => + showOnlyThreatIndicatorAlerts + ? [ + { + meta: { + alias: null, + disabled: false, + negate: false, + key: 'signal.rule.threat_mapping', + type: 'exists', + value: 'exists', + }, + // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] + exists: { field: 'signal.rule.threat_mapping' }, + }, + ] + : []; export const alertsHeaders: ColumnHeaderOptions[] = [ { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index 5c659b7554ec2..be11aecfe47dd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -40,6 +40,8 @@ describe('AlertsTableComponent', () => { clearEventsDeleted={jest.fn()} showBuildingBlockAlerts={false} onShowBuildingBlockAlertsChanged={jest.fn()} + showOnlyThreatIndicatorAlerts={false} + onShowOnlyThreatIndicatorAlertsChanged={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index cf6db52d0cece..2890eb912b84c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -52,22 +52,23 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; interface OwnProps { - timelineId: TimelineIdLiteral; defaultFilters?: Filter[]; - hasIndexWrite: boolean; - hasIndexMaintenance: boolean; from: string; + hasIndexMaintenance: boolean; + hasIndexWrite: boolean; loading: boolean; onRuleChange?: () => void; - showBuildingBlockAlerts: boolean; onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; + onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void; + showBuildingBlockAlerts: boolean; + showOnlyThreatIndicatorAlerts: boolean; + timelineId: TimelineIdLiteral; to: string; } type AlertsTableComponentProps = OwnProps & PropsFromRedux; export const AlertsTableComponent: React.FC = ({ - timelineId, clearEventsDeleted, clearEventsLoading, clearSelected, @@ -75,17 +76,20 @@ export const AlertsTableComponent: React.FC = ({ from, globalFilters, globalQuery, - hasIndexWrite, hasIndexMaintenance, + hasIndexWrite, isSelectAllChecked, loading, loadingEventIds, onRuleChange, + onShowBuildingBlockAlertsChanged, + onShowOnlyThreatIndicatorAlertsChanged, selectedEventIds, setEventsDeleted, setEventsLoading, showBuildingBlockAlerts, - onShowBuildingBlockAlertsChanged, + showOnlyThreatIndicatorAlerts, + timelineId, to, }) => { const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); @@ -264,30 +268,34 @@ export const AlertsTableComponent: React.FC = ({ 0} clearSelection={clearSelectionCallback} - hasIndexWrite={hasIndexWrite} - hasIndexMaintenance={hasIndexMaintenance} currentFilter={filterGroup} + hasIndexMaintenance={hasIndexMaintenance} + hasIndexWrite={hasIndexWrite} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} + onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsChanged} selectAll={selectAllOnAllPagesCallback} selectedEventIds={selectedEventIds} showBuildingBlockAlerts={showBuildingBlockAlerts} - onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} showClearSelection={showClearSelectionAction} + showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} totalCount={totalCount} updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} /> ); }, [ - hasIndexWrite, - hasIndexMaintenance, clearSelectionCallback, filterGroup, - showBuildingBlockAlerts, - onShowBuildingBlockAlertsChanged, + hasIndexMaintenance, + hasIndexWrite, loadingEventIds.length, + onShowBuildingBlockAlertsChanged, + onShowOnlyThreatIndicatorAlertsChanged, selectAllOnAllPagesCallback, selectedEventIds, + showBuildingBlockAlerts, showClearSelectionAction, + showOnlyThreatIndicatorAlerts, updateAlertsStatusCallback, ] ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 8d2f07e19b36a..02e18d09710d7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -50,7 +50,10 @@ import { } from '../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; +import { + buildShowBuildingBlockFilter, + buildThreatMatchFilter, +} from '../../components/alerts_table/default_config'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { NeedAdminForUpdateRulesCallOut } from '../../components/callouts/need_admin_for_update_callout'; @@ -100,6 +103,7 @@ const DetectionEnginePageComponent = () => { const [lastAlerts] = useAlertInfo({}); const { formatUrl } = useFormatUrl(SecurityPageName.detections); const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); + const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); const loading = userInfoLoading || listsConfigLoading; const updateDateRangeCallback = useCallback( @@ -128,14 +132,21 @@ const DetectionEnginePageComponent = () => { ); const alertsHistogramDefaultFilters = useMemo( - () => [...filters, ...buildShowBuildingBlockFilter(showBuildingBlockAlerts)], - [filters, showBuildingBlockAlerts] + () => [ + ...filters, + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), + ], + [filters, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); // AlertsTable manages global filters itself, so not including `filters` const alertsTableDefaultFilters = useMemo( - () => buildShowBuildingBlockFilter(showBuildingBlockAlerts), - [showBuildingBlockAlerts] + () => [ + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), + ], + [showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const onShowBuildingBlockAlertsChangedCallback = useCallback( @@ -145,6 +156,13 @@ const DetectionEnginePageComponent = () => { [setShowBuildingBlockAlerts] ); + const onShowOnlyThreatIndicatorAlertsCallback = useCallback( + (newShowOnlyThreatIndicatorAlerts: boolean) => { + setShowOnlyThreatIndicatorAlerts(newShowOnlyThreatIndicatorAlerts); + }, + [setShowOnlyThreatIndicatorAlerts] + ); + const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections); const onSkipFocusBeforeEventsTable = useCallback(() => { @@ -250,6 +268,8 @@ const DetectionEnginePageComponent = () => { defaultFilters={alertsTableDefaultFilters} showBuildingBlockAlerts={showBuildingBlockAlerts} onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} + showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} + onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsCallback} to={to} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index dddf8ac1bb839..a8d3742bfd600 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -59,6 +59,7 @@ import { StepScheduleRule } from '../../../../components/rules/step_schedule_rul import { buildAlertsRuleIdFilter, buildShowBuildingBlockFilter, + buildThreatMatchFilter, } from '../../../../components/alerts_table/default_config'; import { ReadOnlyAlertsCallOut } from '../../../../components/callouts/read_only_alerts_callout'; import { ReadOnlyRulesCallOut } from '../../../../components/callouts/read_only_rules_callout'; @@ -208,6 +209,7 @@ const RuleDetailsPageComponent = () => { }; const [lastAlerts] = useAlertInfo({ ruleId }); const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); + const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); @@ -286,10 +288,11 @@ const RuleDetailsPageComponent = () => { const alertDefaultFilters = useMemo( () => [ - ...(ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), + ...buildAlertsRuleIdFilter(ruleId), ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [ruleId, showBuildingBlockAlerts] + [ruleId, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ @@ -446,6 +449,13 @@ const RuleDetailsPageComponent = () => { [setShowBuildingBlockAlerts] ); + const onShowOnlyThreatIndicatorAlertsCallback = useCallback( + (newShowOnlyThreatIndicatorAlerts: boolean) => { + setShowOnlyThreatIndicatorAlerts(newShowOnlyThreatIndicatorAlerts); + }, + [setShowOnlyThreatIndicatorAlerts] + ); + const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections); const exceptionLists = useMemo((): { @@ -670,7 +680,9 @@ const RuleDetailsPageComponent = () => { from={from} loading={loading} showBuildingBlockAlerts={showBuildingBlockAlerts} + showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} + onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsCallback} onRuleChange={refreshRule} to={to} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx index f8913148c625b..84406aed3619f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx @@ -35,7 +35,8 @@ jest.mock('../../../../common/components/inspect', () => ({ InspectButtonContainer: jest.fn(({ children }) =>
{children}
), })); -describe('AddTimelineButton', () => { +// FLAKY: https://github.com/elastic/kibana/issues/96691 +describe.skip('AddTimelineButton', () => { let wrapper: ReactWrapper; const props = { timelineId: TimelineId.active, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 87392bce3ee63..50970304953ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -262,7 +262,7 @@ Array [ -ms-flex: 1; flex: 1; overflow: hidden; - padding: 4px 16px 64px; + padding: 4px 16px 50px; } .c0 { @@ -537,7 +537,7 @@ Array [ -ms-flex: 1; flex: 1; overflow: hidden; - padding: 4px 16px 64px; + padding: 4px 16px 50px; } .c0 { @@ -806,7 +806,7 @@ Array [ -ms-flex: 1; flex: 1; overflow: hidden; - padding: 4px 16px 64px; + padding: 4px 16px 50px; } .c0 { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index 435a210b9d260..86175c0e06ad2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -26,7 +26,8 @@ import { BrowserFields } from '../../../../common/containers/source'; import { EventDetails, EventsViewType, - View, + EventView, + ThreatView, } from '../../../../common/components/event_details/event_details'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { LineClamp } from '../../../../common/components/line_clamp'; @@ -87,7 +88,8 @@ ExpandableEventTitle.displayName = 'ExpandableEventTitle'; export const ExpandableEvent = React.memo( ({ browserFields, event, timelineId, timelineTabType, isAlert, loading, detailsData }) => { - const [view, setView] = useState(EventsViewType.summaryView); + const [eventView, setEventView] = useState(EventsViewType.summaryView); + const [threatView, setThreatView] = useState(EventsViewType.threatSummaryView); const message = useMemo(() => { if (detailsData) { @@ -131,10 +133,12 @@ export const ExpandableEvent = React.memo( data={detailsData!} id={event.eventId!} isAlert={isAlert} - onViewSelected={setView} - timelineTabType={timelineTabType} + onThreatViewSelected={setThreatView} + onEventViewSelected={setEventView} + threatView={threatView} timelineId={timelineId} - view={view} + timelineTabType={timelineTabType} + eventView={eventView} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 6f4778f36466b..9a4684193b997 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -25,7 +25,7 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflowContent { flex: 1; overflow: hidden; - padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`}; + padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 50px`}; } } `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 3032f556251f3..e227c87b99870 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -44,7 +44,7 @@ const FormattedFieldValueComponent: React.FC<{ isObjectArray?: boolean; fieldFormat?: string; fieldName: string; - fieldType: string; + fieldType?: string; truncate?: boolean; value: string | number | undefined | null; linkValue?: string | null | undefined; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index f4a5d6add4f41..103e3ae80831a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -37,6 +37,10 @@ import { metadataTransformPrefix } from '../../common/endpoint/constants'; import { AppClientFactory } from '../client'; import { ConfigType } from '../config'; import { LicenseService } from '../../common/license/license'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../../common/experimental_features'; export interface MetadataService { queryStrategy( @@ -107,6 +111,9 @@ export class EndpointAppContextService { private agentPolicyService: AgentPolicyServiceInterface | undefined; private savedObjectsStart: SavedObjectsServiceStart | undefined; private metadataService: MetadataService | undefined; + private config: ConfigType | undefined; + + private experimentalFeatures: ExperimentalFeatures | undefined; public start(dependencies: EndpointAppContextServiceStartContract) { this.agentService = dependencies.agentService; @@ -115,6 +122,9 @@ export class EndpointAppContextService { this.manifestManager = dependencies.manifestManager; this.savedObjectsStart = dependencies.savedObjectsStart; this.metadataService = createMetadataService(dependencies.packageService!); + this.config = dependencies.config; + + this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental); if (this.manifestManager && dependencies.registerIngestCallback) { dependencies.registerIngestCallback( @@ -140,6 +150,10 @@ export class EndpointAppContextService { public stop() {} + public getExperimentalFeatures(): Readonly | undefined { + return this.experimentalFeatures; + } + public getAgentService(): AgentService | undefined { return this.agentService; } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 65bd6ffd15f5f..7cfcf11379dd8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -22,6 +22,9 @@ export const ArtifactConstants = { SUPPORTED_OPERATING_SYSTEMS: ['macos', 'windows'], SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], GLOBAL_TRUSTED_APPS_NAME: 'endpoint-trustlist', + + SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], + GLOBAL_EVENT_FILTERS_NAME: 'endpoint-eventfilterlist', }; export const ManifestConstants = { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 322bb2ca47a45..1c3c92c50afd3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -14,20 +14,21 @@ import { Entry, EntryNested } from '../../../../../lists/common/schemas/types'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../common/shared_imports'; import { + internalArtifactCompleteSchema, + InternalArtifactCompleteSchema, InternalArtifactSchema, TranslatedEntry, - WrappedTranslatedExceptionList, - wrappedTranslatedExceptionList, - TranslatedEntryNestedEntry, - translatedEntryNestedEntry, translatedEntry as translatedEntryType, + translatedEntryMatchAnyMatcher, TranslatedEntryMatcher, translatedEntryMatchMatcher, - translatedEntryMatchAnyMatcher, + TranslatedEntryNestedEntry, + translatedEntryNestedEntry, TranslatedExceptionListItem, - internalArtifactCompleteSchema, - InternalArtifactCompleteSchema, + WrappedTranslatedExceptionList, + wrappedTranslatedExceptionList, } from '../../schemas'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../../lists/common/constants'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, @@ -77,7 +78,10 @@ export async function getFilteredEndpointExceptionList( eClient: ExceptionListClient, schemaVersion: string, filter: string, - listId: typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID + listId: + | typeof ENDPOINT_LIST_ID + | typeof ENDPOINT_TRUSTED_APPS_LIST_ID + | typeof ENDPOINT_EVENT_FILTERS_LIST_ID ): Promise { const exceptions: WrappedTranslatedExceptionList = { entries: [] }; let page = 1; @@ -142,6 +146,27 @@ export async function getEndpointTrustedAppsList( ); } +export async function getEndpointEventFiltersList( + eClient: ExceptionListClient, + schemaVersion: string, + os: string, + policyId?: string +): Promise { + const osFilter = `exception-list-agnostic.attributes.os_types:\"${os}\"`; + const policyFilter = `(exception-list-agnostic.attributes.tags:\"policy:all\"${ + policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : '' + })`; + + await eClient.createEndpointEventFiltersList(); + + return getFilteredEndpointExceptionList( + eClient, + schemaVersion, + `${osFilter} and ${policyFilter}`, + ENDPOINT_EVENT_FILTERS_LIST_ID + ); +} + /** * Translates Exception list items to Exceptions the endpoint can understand * @param exceptions diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts index d0ad6e4734baf..cf1f178a80e78 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts @@ -66,8 +66,8 @@ describe('When migrating artifacts to fleet', () => { it('should do nothing if `fleetServerEnabled` flag is false', async () => { await migrateArtifactsToFleet(soClient, artifactClient, logger, false); - expect(logger.info).toHaveBeenCalledWith( - 'Skipping Artifacts migration to fleet. [fleetServerEnabled] flag is off' + expect(logger.debug).toHaveBeenCalledWith( + 'Skipping Artifacts migration. [fleetServerEnabled] flag is off' ); expect(soClient.find).not.toHaveBeenCalled(); }); @@ -94,7 +94,7 @@ describe('When migrating artifacts to fleet', () => { const error = new Error('test: delete failed'); soClient.delete.mockRejectedValue(error); await expect(migrateArtifactsToFleet(soClient, artifactClient, logger, true)).rejects.toThrow( - 'Artifact SO migration to fleet failed' + 'Artifact SO migration failed' ); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts index bcbcb7f63e3ca..ba3c15cecf217 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts @@ -27,7 +27,7 @@ export const migrateArtifactsToFleet = async ( isFleetServerEnabled: boolean ): Promise => { if (!isFleetServerEnabled) { - logger.info('Skipping Artifacts migration to fleet. [fleetServerEnabled] flag is off'); + logger.debug('Skipping Artifacts migration. [fleetServerEnabled] flag is off'); return; } @@ -49,14 +49,16 @@ export const migrateArtifactsToFleet = async ( if (totalArtifactsMigrated === -1) { totalArtifactsMigrated = total; if (total > 0) { - logger.info(`Migrating artifacts from SavedObject to Fleet`); + logger.info(`Migrating artifacts from SavedObject`); } } // If nothing else to process, then exit out if (total === 0) { hasMore = false; - logger.info(`Total Artifacts migrated to Fleet: ${totalArtifactsMigrated}`); + if (totalArtifactsMigrated > 0) { + logger.info(`Total Artifacts migrated: ${totalArtifactsMigrated}`); + } return; } @@ -78,7 +80,7 @@ export const migrateArtifactsToFleet = async ( } } } catch (e) { - const error = new ArtifactMigrationError('Artifact SO migration to fleet failed', e); + const error = new ArtifactMigrationError('Artifact SO migration failed', e); logger.error(error); throw error; } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index c82d2b6524773..d1911a39166dc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -56,6 +56,7 @@ export const createMockEndpointAppContextService = ( return ({ start: jest.fn(), stop: jest.fn(), + getExperimentalFeatures: jest.fn(), getAgentService: jest.fn(), getAgentPolicyService: jest.fn(), getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()), diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index ececb425af657..6f41fe3578496 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -22,6 +22,7 @@ import { } from '../../../lib/artifacts/mocks'; import { createEndpointArtifactClientMock, getManifestClientMock } from '../mocks'; import { ManifestManager, ManifestManagerContext } from './manifest_manager'; +import { parseExperimentalConfigValue } from '../../../../../common/experimental_features'; export const createExceptionListResponse = (data: ExceptionListItemSchema[], total?: number) => ({ data, @@ -85,6 +86,7 @@ export const buildManifestManagerContextMock = ( ...fullOpts, artifactClient: createEndpointArtifactClientMock(), logger: loggingSystemMock.create().get() as jest.Mocked, + experimentalFeatures: parseExperimentalConfigValue([]), }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 9ed17686fd2bc..b3d8b63687d31 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -22,6 +22,7 @@ import { ArtifactConstants, buildArtifact, getArtifactId, + getEndpointEventFiltersList, getEndpointExceptionList, getEndpointTrustedAppsList, isCompressed, @@ -34,6 +35,7 @@ import { } from '../../../schemas/artifacts'; import { EndpointArtifactClientInterface } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; interface ArtifactsBuildResult { defaultArtifacts: InternalArtifactCompleteSchema[]; @@ -81,6 +83,7 @@ export interface ManifestManagerContext { packagePolicyService: PackagePolicyServiceInterface; logger: Logger; cache: LRU; + experimentalFeatures: ExperimentalFeatures; } const getArtifactIds = (manifest: ManifestSchema) => @@ -99,11 +102,9 @@ export class ManifestManager { protected logger: Logger; protected cache: LRU; protected schemaVersion: ManifestSchemaVersion; + protected experimentalFeatures: ExperimentalFeatures; - constructor( - context: ManifestManagerContext, - private readonly isFleetServerEnabled: boolean = false - ) { + constructor(context: ManifestManagerContext) { this.artifactClient = context.artifactClient; this.exceptionListClient = context.exceptionListClient; this.packagePolicyService = context.packagePolicyService; @@ -111,6 +112,7 @@ export class ManifestManager { this.logger = context.logger; this.cache = context.cache; this.schemaVersion = 'v1'; + this.experimentalFeatures = context.experimentalFeatures; } /** @@ -198,6 +200,41 @@ export class ManifestManager { return { defaultArtifacts, policySpecificArtifacts }; } + /** + * Builds an array of endpoint event filters (one per supported OS) based on the current state of the + * Event Filters list + * @protected + */ + protected async buildEventFiltersArtifacts(): Promise { + const defaultArtifacts: InternalArtifactCompleteSchema[] = []; + const policySpecificArtifacts: Record = {}; + + for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { + defaultArtifacts.push(await this.buildEventFiltersForOs(os)); + } + + await iterateAllListItems( + (page) => this.listEndpointPolicyIds(page), + async (policyId) => { + for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { + policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; + policySpecificArtifacts[policyId].push(await this.buildEventFiltersForOs(os, policyId)); + } + } + ); + + return { defaultArtifacts, policySpecificArtifacts }; + } + + protected async buildEventFiltersForOs(os: string, policyId?: string) { + return buildArtifact( + await getEndpointEventFiltersList(this.exceptionListClient, this.schemaVersion, os, policyId), + this.schemaVersion, + os, + ArtifactConstants.GLOBAL_EVENT_FILTERS_NAME + ); + } + /** * Writes new artifact SO. * @@ -286,7 +323,7 @@ export class ManifestManager { semanticVersion: manifestSo.attributes.semanticVersion, soVersion: manifestSo.version, }, - this.isFleetServerEnabled + this.experimentalFeatures.fleetServerEnabled ); for (const entry of manifestSo.attributes.artifacts) { @@ -327,12 +364,16 @@ export class ManifestManager { public async buildNewManifest( baselineManifest: Manifest = ManifestManager.createDefaultManifest( this.schemaVersion, - this.isFleetServerEnabled + this.experimentalFeatures.fleetServerEnabled ) ): Promise { const results = await Promise.all([ this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts(), + // If Endpoint Event Filtering feature is ON, then add in the exceptions for them + ...(this.experimentalFeatures.eventFilteringEnabled + ? [this.buildEventFiltersArtifacts()] + : []), ]); const manifest = new Manifest( @@ -341,7 +382,7 @@ export class ManifestManager { semanticVersion: baselineManifest.getSemanticVersion(), soVersion: baselineManifest.getSavedObjectVersion(), }, - this.isFleetServerEnabled + this.experimentalFeatures.fleetServerEnabled ); for (const result of results) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md index 901dacbfe80cc..1b8516ee16012 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/README.md @@ -4,7 +4,7 @@ -1. [Have the env params set up](https://github.com/elastic/kibana/blob/master/x-pack/plugins/siem/server/lib/detection_engine/README.md) +1. [Have the env params set up](https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/server/lib/detection_engine/README.md) 2. Create a new timelines template into `x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines` diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 04f98e53ea9a3..8dab308affad8 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -349,24 +349,22 @@ export class Plugin implements IPlugin { @@ -376,7 +374,7 @@ export class Plugin implements IPlugin { - logger.info('Fleet setup complete - Starting ManifestTask'); + logger.info('Dependent plugin setup complete - Starting ManifestTask'); if (this.manifestTask) { this.manifestTask.start({ diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts index 4dab0ebc43149..0b418c0da410c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts @@ -7,7 +7,7 @@ import { mapValues, isObject, isArray } from 'lodash/fp'; -import { toArray } from './to_array'; +import { toArray } from '../../../common/utils/to_array'; export const mapObjectValuesToStringArray = (object: object): object => mapValues((o) => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 3f4eb5721164b..bed4a040f92b0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -14,8 +14,7 @@ import { HostsEdges, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; - -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; export const HOSTS_FIELDS: readonly string[] = [ '_id', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index aeaefe690cbde..807b78cb9cdd2 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -8,7 +8,7 @@ import { get, getOr, isEmpty } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { AuthenticationsEdges, AuthenticationHit, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index d36af61957690..00ed5c0c0dc01 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -8,6 +8,7 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { Direction } from '../../../../../../common/search_strategy/common'; import { AggregationRequest, @@ -16,7 +17,6 @@ import { HostItem, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; export const HOST_FIELDS = [ '_id', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index fe202b48540d7..1c1e2111f3771 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -14,7 +14,7 @@ import { HostsUncommonProcessesEdges, HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; export const uncommonProcessesFields = [ diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts index 8fc7ae0304a35..cc1bfdff8e096 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts @@ -13,7 +13,7 @@ import { NetworkDetailsHostHit, NetworkHit, } from '../../../../../../common/search_strategy/security_solution/network'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; export const getNetworkDetailsAgg = (type: string, networkHit: NetworkHit | {}) => { const firstSeen = getOr(null, `firstSeen.value_as_string`, networkHit); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts index 15d0e2d5494b8..29b0df9e4bbf7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts @@ -5,6 +5,15 @@ * 2.0. */ +export const TIMELINE_CTI_FIELDS = [ + 'threat.indicator.event.dataset', + 'threat.indicator.event.reference', + 'threat.indicator.matched.atomic', + 'threat.indicator.matched.field', + 'threat.indicator.matched.type', + 'threat.indicator.provider', +]; + export const TIMELINE_EVENTS_FIELDS = [ '@timestamp', 'signal.status', @@ -230,4 +239,5 @@ export const TIMELINE_EVENTS_FIELDS = [ 'zeek.ssl.established', 'zeek.ssl.resumed', 'zeek.ssl.version', + ...TIMELINE_CTI_FIELDS, ]; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts index 61af6a7664faa..da19df32ac87a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -5,10 +5,10 @@ * 2.0. */ +import { eventHit } from '../../../../../../common/utils/mock_event_details'; import { EventHit } from '../../../../../../common/search_strategy'; import { TIMELINE_EVENTS_FIELDS } from './constants'; -import { formatTimelineData } from './helpers'; -import { eventHit } from '../mocks'; +import { buildObjectForFieldPath, formatTimelineData } from './helpers'; describe('#formatTimelineData', () => { it('happy path', async () => { @@ -42,12 +42,12 @@ describe('#formatTimelineData', () => { value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], }, { - field: 'source.geo.location', - value: [`{"lon":118.7778,"lat":32.0617}`], + field: 'threat.indicator.matched.field', + value: ['matched_field', 'other_matched_field', 'matched_field_2'], }, { - field: 'threat.indicator.matched.field', - value: ['matched_field', 'matched_field_2'], + field: 'source.geo.location', + value: [`{"lon":118.7778,"lat":32.0617}`], }, ], ecs: { @@ -94,6 +94,34 @@ describe('#formatTimelineData', () => { user: { name: ['jenkins'], }, + threat: { + indicator: [ + { + event: { + dataset: [], + reference: [], + }, + matched: { + atomic: ['matched_atomic'], + field: ['matched_field', 'other_matched_field'], + type: [], + }, + provider: ['yourself'], + }, + { + event: { + dataset: [], + reference: [], + }, + matched: { + atomic: ['matched_atomic_2'], + field: ['matched_field_2'], + type: [], + }, + provider: ['other_you'], + }, + ], + }, }, }, }); @@ -371,4 +399,173 @@ describe('#formatTimelineData', () => { }, }); }); + + describe('buildObjectForFieldPath', () => { + it('builds an object from a single non-nested field', () => { + expect(buildObjectForFieldPath('@timestamp', eventHit)).toEqual({ + '@timestamp': ['2020-11-17T14:48:08.922Z'], + }); + }); + + it('builds an object with no fields response', () => { + const { fields, ...fieldLessHit } = eventHit; + // @ts-expect-error fieldLessHit is intentionally missing fields + expect(buildObjectForFieldPath('@timestamp', fieldLessHit)).toEqual({ + '@timestamp': [], + }); + }); + + it('does not misinterpret non-nested fields with a common prefix', () => { + // @ts-expect-error hit is minimal + const hit: EventHit = { + fields: { + 'foo.bar': ['baz'], + 'foo.barBaz': ['foo'], + }, + }; + + expect(buildObjectForFieldPath('foo.barBaz', hit)).toEqual({ + foo: { barBaz: ['foo'] }, + }); + }); + + it('builds an array of objects from a nested field', () => { + // @ts-expect-error hit is minimal + const hit: EventHit = { + fields: { + foo: [{ bar: ['baz'] }], + }, + }; + expect(buildObjectForFieldPath('foo.bar', hit)).toEqual({ + foo: [{ bar: ['baz'] }], + }); + }); + + it('builds intermediate objects for nested fields', () => { + // @ts-expect-error nestedHit is minimal + const nestedHit: EventHit = { + fields: { + 'foo.bar': [ + { + baz: ['host.name'], + }, + ], + }, + }; + expect(buildObjectForFieldPath('foo.bar.baz', nestedHit)).toEqual({ + foo: { + bar: [ + { + baz: ['host.name'], + }, + ], + }, + }); + }); + + it('builds intermediate objects at multiple levels', () => { + expect(buildObjectForFieldPath('threat.indicator.matched.atomic', eventHit)).toEqual({ + threat: { + indicator: [ + { + matched: { + atomic: ['matched_atomic'], + }, + }, + { + matched: { + atomic: ['matched_atomic_2'], + }, + }, + ], + }, + }); + }); + + it('preserves multiple values for a single leaf', () => { + expect(buildObjectForFieldPath('threat.indicator.matched.field', eventHit)).toEqual({ + threat: { + indicator: [ + { + matched: { + field: ['matched_field', 'other_matched_field'], + }, + }, + { + matched: { + field: ['matched_field_2'], + }, + }, + ], + }, + }); + }); + + describe('multiple levels of nested fields', () => { + let nestedHit: EventHit; + + beforeEach(() => { + // @ts-expect-error nestedHit is minimal + nestedHit = { + fields: { + 'nested_1.foo': [ + { + 'nested_2.bar': [ + { leaf: ['leaf_value'], leaf_2: ['leaf_2_value'] }, + { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] }, + ], + }, + { + 'nested_2.bar': [ + { leaf: ['leaf_value_2'], leaf_2: ['leaf_2_value_4'] }, + { leaf: ['leaf_value_3'], leaf_2: ['leaf_2_value_5'] }, + ], + }, + ], + }, + }; + }); + + it('includes objects without the field', () => { + expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf', nestedHit)).toEqual({ + nested_1: { + foo: [ + { + nested_2: { + bar: [{ leaf: ['leaf_value'] }, { leaf: [] }], + }, + }, + { + nested_2: { + bar: [{ leaf: ['leaf_value_2'] }, { leaf: ['leaf_value_3'] }], + }, + }, + ], + }, + }); + }); + + it('groups multiple leaf values', () => { + expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf_2', nestedHit)).toEqual({ + nested_1: { + foo: [ + { + nested_2: { + bar: [ + { leaf_2: ['leaf_2_value'] }, + { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] }, + ], + }, + }, + { + nested_2: { + bar: [{ leaf_2: ['leaf_2_value_4'] }, { leaf_2: ['leaf_2_value_5'] }], + }, + }, + ], + }, + }); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index e5bb8cb7e14b7..6c20843058ff1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -5,14 +5,20 @@ * 2.0. */ +import { set } from '@elastic/safer-lodash-set'; import { get, has, merge, uniq } from 'lodash/fp'; +import { Ecs } from '../../../../../../common/ecs'; import { EventHit, + Fields, TimelineEdges, TimelineNonEcsData, } from '../../../../../../common/search_strategy'; -import { toStringArray } from '../../../../helpers/to_array'; -import { getDataSafety, getDataFromFieldsHits } from '../details/helpers'; +import { toStringArray } from '../../../../../../common/utils/to_array'; +import { + getDataFromFieldsHits, + getDataSafety, +} from '../../../../../../common/utils/field_formatters'; const getTimestamp = (hit: EventHit): string => { if (hit.fields && hit.fields['@timestamp']) { @@ -75,18 +81,13 @@ const getValuesFromFields = async ( [fieldName]: get(fieldName, hit._source), }; } else { - if (nestedParentFieldName == null || nestedParentFieldName === fieldName) { + if (nestedParentFieldName == null) { fieldToEval = { [fieldName]: hit.fields[fieldName], }; - } else if (nestedParentFieldName != null) { - fieldToEval = { - [nestedParentFieldName]: hit.fields[nestedParentFieldName], - }; } else { - // fallback, should never hit fieldToEval = { - [fieldName]: [], + [nestedParentFieldName]: hit.fields[nestedParentFieldName], }; } } @@ -99,6 +100,37 @@ const getValuesFromFields = async ( ); }; +const buildObjectRecursive = (fieldPath: string, fields: Fields): Partial => { + const nestedParentPath = getNestedParentPath(fieldPath, fields); + if (!nestedParentPath) { + return set({}, fieldPath, toStringArray(get(fieldPath, fields))); + } + + const subPath = fieldPath.replace(`${nestedParentPath}.`, ''); + const subFields = (get(nestedParentPath, fields) ?? []) as Fields[]; + return set( + {}, + nestedParentPath, + subFields.map((subField) => buildObjectRecursive(subPath, subField)) + ); +}; + +export const buildObjectForFieldPath = (fieldPath: string, hit: EventHit): Partial => { + if (has(fieldPath, hit._source)) { + const value = get(fieldPath, hit._source); + return set({}, fieldPath, toStringArray(value)); + } + + return buildObjectRecursive(fieldPath, hit.fields); +}; + +/** + * If a prefix of our full field path is present as a field, we know that our field is nested + */ +const getNestedParentPath = (fieldPath: string, fields: Fields | undefined): string | undefined => + fields && + Object.keys(fields).find((field) => field !== fieldPath && fieldPath.startsWith(`${field}.`)); + const mergeTimelineFieldsWithHit = async ( fieldName: string, flattenedFields: T, @@ -106,15 +138,12 @@ const mergeTimelineFieldsWithHit = async ( dataFields: readonly string[], ecsFields: readonly string[] ) => { - if (fieldName != null || dataFields.includes(fieldName)) { - const fieldNameAsArray = fieldName.split('.'); - const nestedParentFieldName = Object.keys(hit.fields ?? []).find((f) => { - return f === fieldNameAsArray.slice(0, f.split('.').length).join('.'); - }); + if (fieldName != null) { + const nestedParentPath = getNestedParentPath(fieldName, hit.fields); if ( + nestedParentPath != null || has(fieldName, hit._source) || has(fieldName, hit.fields) || - nestedParentFieldName != null || specialFields.includes(fieldName) ) { const objectWithProperty = { @@ -123,22 +152,13 @@ const mergeTimelineFieldsWithHit = async ( data: dataFields.includes(fieldName) ? [ ...get('node.data', flattenedFields), - ...(await getValuesFromFields(fieldName, hit, nestedParentFieldName)), + ...(await getValuesFromFields(fieldName, hit, nestedParentPath)), ] : get('node.data', flattenedFields), ecs: ecsFields.includes(fieldName) ? { ...get('node.ecs', flattenedFields), - // @ts-expect-error - ...fieldName.split('.').reduceRight( - // @ts-expect-error - (obj, next) => ({ [next]: obj }), - toStringArray( - has(fieldName, hit._source) - ? get(fieldName, hit._source) - : hit.fields[fieldName] - ) - ), + ...buildObjectForFieldPath(fieldName, hit), } : get('node.ecs', flattenedFields), }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 0107ba44baec7..a4d6eebfb71b8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -19,7 +19,11 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; -import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; +import { + getDataFromFieldsHits, + getDataFromSourceHits, + getDataSafety, +} from '../../../../../../common/utils/field_formatters'; export const timelineEventsDetails: SecuritySolutionTimelineFactory = { buildDsl: (options: TimelineEventsDetailsRequestOptions) => { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bde44733d5a74..cc32660c6724c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5453,7 +5453,6 @@ "xpack.apm.serviceOverview.mlNudgeMessage.content": "APM の異常検知統合で、異常なトランザクションを特定し、アップストリームおよびダウンストリームサービスの正常性を確認します。わずか数分で開始できます。", "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "閉じる", "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "使ってみる", - "xpack.apm.serviceOverview.searchBar.transactionTypeLabel": "タイプ:{transactionType}", "xpack.apm.serviceOverview.throughtputChartTitle": "スループット", "xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "エラー率", "xpack.apm.serviceOverview.transactionsTableColumnImpact": "インパクト", @@ -5653,7 +5652,6 @@ "xpack.apm.transactionDurationLabel": "期間", "xpack.apm.transactionErrorRateAlert.name": "トランザクションエラー率しきい値", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "より大きい", - "xpack.apm.transactionOverviewTitle": "トランザクション", "xpack.apm.transactionRateLabel": "{value} tpm", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "95 パーセンタイル", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "99 パーセンタイル", @@ -13710,7 +13708,8 @@ "xpack.ml.editModelSnapshotFlyout.useDefaultButton": "削除", "xpack.ml.explorer.addToDashboard.cancelButtonLabel": "キャンセル", "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "ダッシュボードを選択:", - "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "スイムレーンビューを選択:", + "xpack.ml.explorer.addToDashboard.swimlanes.dashboardsTitle": "スイムレーンをダッシュボードに追加", + "xpack.ml.explorer.addToDashboard.swimlanes.selectSwimlanesLabel": "スイムレーンビューを選択:", "xpack.ml.explorer.addToDashboardLabel": "ダッシュボードに追加", "xpack.ml.explorer.annotationsErrorCallOutTitle": "注釈の読み込み中にエラーが発生しました。", "xpack.ml.explorer.annotationsErrorTitle": "注釈", @@ -13735,7 +13734,6 @@ "xpack.ml.explorer.dashboardsTable.descriptionColumnHeader": "説明", "xpack.ml.explorer.dashboardsTable.savedSuccessfullyTitle": "ダッシュボード「{dashboardTitle}」は正常に更新されました", "xpack.ml.explorer.dashboardsTable.titleColumnHeader": "タイトル", - "xpack.ml.explorer.dashboardsTitle": "スイムレーンをダッシュボードに追加", "xpack.ml.explorer.distributionChart.anomalyScoreLabel": "異常スコア", "xpack.ml.explorer.distributionChart.entityLabel": "エンティティ", "xpack.ml.explorer.distributionChart.typicalLabel": "通常", @@ -14201,7 +14199,6 @@ "xpack.ml.jobsList.refreshButtonLabel": "更新", "xpack.ml.jobsList.resultActions.openJobsInAnomalyExplorerText": "{jobsCount, plural, one {{jobId}} other {# 件のジョブ}} を異常エクスプローラーで開く", "xpack.ml.jobsList.resultActions.openJobsInSingleMetricViewerText": "シングルメトリックビューアーで {jobsCount, plural, one {{jobId}} other {# 件のジョブ}} を開く", - "xpack.ml.jobsList.resultActions.singleMetricDisabledMessageText": "{reason}のため無効です。", "xpack.ml.jobsList.selectRowForJobMessage": "ジョブID {jobId} の行を選択", "xpack.ml.jobsList.showDetailsColumn.screenReaderDescription": "このカラムには各ジョブの詳細を示すクリック可能なコントロールが含まれます", "xpack.ml.jobsList.spacesLabel": "スペース", @@ -15068,7 +15065,6 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel": "ズーム:", "xpack.ml.timeSeriesExplorer.tryWideningTheTimeSelectionDescription": "時間範囲を広げるか、さらに過去に遡ってみてください。", "xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage": "このダッシュボードでは 1 度に 1 つのジョブしか表示できません", - "xpack.ml.timeSeriesJob.jobWithRunTimeMessage": "データフィードにはランタイムフィールドが含まれ、モデルプロットが無効です", "xpack.ml.timeSeriesJob.notViewableTimeSeriesJobMessage": "表示可能な時系列ジョブではありません", "xpack.ml.timeSeriesJob.sourceDataModelPlotNotChartableMessage": "この検出器ではソースデータとモデルプロットの両方をグラフ化できません", "xpack.ml.timeSeriesJob.sourceDataNotChartableWithDisabledModelPlotMessage": "この検出器ではソースデータを表示できません。モデルプロットが無効です", @@ -22888,7 +22884,6 @@ "xpack.uptime.certs.status.ok.label": " {okRelativeDate}", "xpack.uptime.charts.mlAnnotation.header": "スコア:{score}", "xpack.uptime.charts.mlAnnotation.severity": "深刻度:{severity}", - "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "オブザーバー位置情報マップを監視", "xpack.uptime.controls.selectSeverity.criticalLabel": "致命的", "xpack.uptime.controls.selectSeverity.majorLabel": "メジャー", "xpack.uptime.controls.selectSeverity.minorLabel": "マイナー", @@ -22918,12 +22913,7 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "{title} を検索", "xpack.uptime.filterPopover.filterItem.label": "{title} {item}でフィルタリングします。", "xpack.uptime.integrationLink.missingDataMessage": "この統合に必要なデータが見つかりませんでした。", - "xpack.uptime.locationAvailabilityViewToggleLegend": "トグルを表示", - "xpack.uptime.locationMap.locations.missing.message": "重要な位置情報構成がありません。{codeBlock}フィールドを使用して、アップタイムチェック用に一意の地域を作成できます。", - "xpack.uptime.locationMap.locations.missing.message1": "詳細については、ドキュメンテーションを参照してください。", - "xpack.uptime.locationMap.locations.missing.title": "地理情報の欠測", "xpack.uptime.locationName.helpLinkAnnotation": "場所を追加", - "xpack.uptime.mapToolTip.AvailabilityStat.title": "{value} %", "xpack.uptime.ml.durationChart.exploreInMlApp": "ML アプリで探索", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "異常検知", "xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 82bd66d85e607..ae2bb6c146dcd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5486,7 +5486,6 @@ "xpack.apm.serviceOverview.mlNudgeMessage.content": "通过 APM 的异常检测集成来查明异常事务,并了解上下游服务的运行状况。只需几分钟即可开始使用。", "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "关闭", "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "开始使用", - "xpack.apm.serviceOverview.searchBar.transactionTypeLabel": "类型:{transactionType}", "xpack.apm.serviceOverview.throughtputChartTitle": "吞吐量", "xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "错误率", "xpack.apm.serviceOverview.transactionsTableColumnImpact": "影响", @@ -5691,7 +5690,6 @@ "xpack.apm.transactionDurationLabel": "持续时间", "xpack.apm.transactionErrorRateAlert.name": "事务错误率阈值", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "高于", - "xpack.apm.transactionOverviewTitle": "事务", "xpack.apm.transactionRateLabel": "{value} tpm", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "第 95 个百分位", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "第 99 个百分位", @@ -13889,7 +13887,8 @@ "xpack.ml.editModelSnapshotFlyout.useDefaultButton": "删除", "xpack.ml.explorer.addToDashboard.cancelButtonLabel": "取消", "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "选择仪表板:", - "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "选择泳道视图:", + "xpack.ml.explorer.addToDashboard.swimlanes.dashboardsTitle": "将泳道添加到仪表板", + "xpack.ml.explorer.addToDashboard.swimlanes.selectSwimlanesLabel": "选择泳道视图:", "xpack.ml.explorer.addToDashboardLabel": "添加到仪表板", "xpack.ml.explorer.annotationsErrorCallOutTitle": "加载注释时发生错误:", "xpack.ml.explorer.annotationsErrorTitle": "标注", @@ -13914,7 +13913,6 @@ "xpack.ml.explorer.dashboardsTable.descriptionColumnHeader": "描述", "xpack.ml.explorer.dashboardsTable.savedSuccessfullyTitle": "仪表板“{dashboardTitle}”已成功更新", "xpack.ml.explorer.dashboardsTable.titleColumnHeader": "标题", - "xpack.ml.explorer.dashboardsTitle": "将泳道添加到仪表板", "xpack.ml.explorer.distributionChart.anomalyScoreLabel": "异常分数", "xpack.ml.explorer.distributionChart.entityLabel": "实体", "xpack.ml.explorer.distributionChart.typicalLabel": "典型", @@ -14397,7 +14395,6 @@ "xpack.ml.jobsList.refreshButtonLabel": "刷新", "xpack.ml.jobsList.resultActions.openJobsInAnomalyExplorerText": "在 Anomaly Explorer 中打开 {jobsCount, plural, one {{jobId}} other {# 个作业}}", "xpack.ml.jobsList.resultActions.openJobsInSingleMetricViewerText": "在 Single Metric Viewer 中打开 {jobsCount, plural, one {{jobId}} other {# 个作业}}", - "xpack.ml.jobsList.resultActions.singleMetricDisabledMessageText": "由于{reason},已禁用。", "xpack.ml.jobsList.selectRowForJobMessage": "选择作业 ID {jobId} 的行", "xpack.ml.jobsList.showDetailsColumn.screenReaderDescription": "此列包含可单击控件,用于显示每个作业的更多详情", "xpack.ml.jobsList.spacesLabel": "工作区", @@ -15285,7 +15282,6 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel": "缩放:", "xpack.ml.timeSeriesExplorer.tryWideningTheTimeSelectionDescription": "请尝试扩大时间选择范围或进一步向后追溯。", "xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage": "在此仪表板中,一次仅可以查看一个作业", - "xpack.ml.timeSeriesJob.jobWithRunTimeMessage": "数据馈送包含运行时字段,模型绘图已禁用", "xpack.ml.timeSeriesJob.notViewableTimeSeriesJobMessage": "不是可查看的时间序列作业", "xpack.ml.timeSeriesJob.sourceDataModelPlotNotChartableMessage": "此检测器的源数据和模型绘图均无法绘制", "xpack.ml.timeSeriesJob.sourceDataNotChartableWithDisabledModelPlotMessage": "此检测器的源数据无法查看,且模型绘图处于禁用状态", @@ -23246,7 +23242,6 @@ "xpack.uptime.certs.status.ok.label": " 对于 {okRelativeDate}", "xpack.uptime.charts.mlAnnotation.header": "分数:{score}", "xpack.uptime.charts.mlAnnotation.severity": "严重性:{severity}", - "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "监测观察者位置地图", "xpack.uptime.controls.selectSeverity.criticalLabel": "紧急", "xpack.uptime.controls.selectSeverity.majorLabel": "重大", "xpack.uptime.controls.selectSeverity.minorLabel": "轻微", @@ -23276,12 +23271,7 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "搜索 {title}", "xpack.uptime.filterPopover.filterItem.label": "按 {title} {item} 筛选。", "xpack.uptime.integrationLink.missingDataMessage": "未找到此集成的所需数据。", - "xpack.uptime.locationAvailabilityViewToggleLegend": "视图切换", - "xpack.uptime.locationMap.locations.missing.message": "重要的地理位置配置缺失。您可以使用 {codeBlock} 字段为您的运行时间检查创建独特的地理区域。", - "xpack.uptime.locationMap.locations.missing.message1": "在我们的文档中获取更多的信息。", - "xpack.uptime.locationMap.locations.missing.title": "地理信息缺失", "xpack.uptime.locationName.helpLinkAnnotation": "添加位置", - "xpack.uptime.mapToolTip.AvailabilityStat.title": "{value} %", "xpack.uptime.ml.durationChart.exploreInMlApp": "在 ML 应用中浏览", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "异常检测", "xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel": "取消", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx index 95fbe9c6ae614..b774fd702fadc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx @@ -49,7 +49,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [

@@ -62,7 +62,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ inputDisplay: i18n.translate( 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.display', { - defaultMessage: 'Every time rule is active', + defaultMessage: 'Every time alert is active', } ), 'data-test-subj': 'onActiveAlert', @@ -70,14 +70,14 @@ const NOTIFY_WHEN_OPTIONS: Array> = [

diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts index 7b651b6a91951..e896a165916fc 100644 --- a/x-pack/plugins/uptime/common/runtime_types/network_events.ts +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -21,8 +21,8 @@ const NetworkTimingsType = t.type({ }); const CertificateDataType = t.partial({ - validFrom: t.number, - validTo: t.number, + validFrom: t.string, + validTo: t.string, issuer: t.string, subjectName: t.string, }); @@ -41,7 +41,6 @@ const NetworkEventType = t.intersection([ method: t.string, status: t.number, mimeType: t.string, - requestStartTime: t.number, responseHeaders: t.record(t.string, t.string), requestHeaders: t.record(t.string, t.string), timings: NetworkTimingsType, @@ -55,6 +54,7 @@ export type NetworkEvent = t.TypeOf; export const SyntheticsNetworkEventsApiResponseType = t.type({ events: t.array(NetworkEventType), total: t.number, + isWaterfallSupported: t.boolean, }); export type SyntheticsNetworkEventsApiResponse = t.TypeOf< diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 426d3f1f10db8..4ba836c1e5d26 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,8 +1,16 @@ { - "configPath": ["xpack", "uptime"], + "configPath": [ + "xpack", + "uptime" + ], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": ["data", "home", "observability", "ml"], + "optionalPlugins": [ + "data", + "home", + "observability", + "ml" + ], "requiredPlugins": [ "alerting", "embeddable", @@ -14,5 +22,12 @@ "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "maps"] -} + "requiredBundles": [ + "observability", + "kibanaReact", + "kibanaUtils", + "home", + "data", + "ml" + ] +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/components/monitor/index.ts b/x-pack/plugins/uptime/public/components/monitor/index.ts index 73ac77a61461f..2c95ac3347723 100644 --- a/x-pack/plugins/uptime/public/components/monitor/index.ts +++ b/x-pack/plugins/uptime/public/components/monitor/index.ts @@ -7,7 +7,6 @@ export * from './ml'; export * from './ping_list'; -export * from './status_details/location_map'; export * from './status_details'; export * from './ping_histogram'; export * from './monitor_charts'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap deleted file mode 100644 index 517dd45cdfdc6..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/__snapshots__/location_availability.test.tsx.snap +++ /dev/null @@ -1,280 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationAvailability component doesnt shows warning if geo is provided 1`] = ` - - - - -

- Monitoring from -

-
-
- - - -
- - - - - -
-`; - -exports[`LocationAvailability component renders correctly against snapshot 1`] = ` - - - - -

- Monitoring from -

-
-
- - - -
- - - - - -
-`; - -exports[`LocationAvailability component renders named locations that have missing geo data 1`] = ` - - - - -

- Monitoring from -

-
-
- - - -
- - - - - -
-`; - -exports[`LocationAvailability component shows warning if geo information is missing 1`] = ` - - - - - - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx index 3190bde153f45..855b8ef0c9767 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.test.tsx @@ -6,45 +6,16 @@ */ import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; +import { screen } from '@testing-library/react'; +import { render } from '../../../../lib/helper/rtl_helpers'; import { LocationAvailability } from './location_availability'; import { MonitorLocations } from '../../../../../common/runtime_types'; -import { LocationMissingWarning } from '../location_map/location_missing'; - -class LocalStorageMock { - store: Record; - constructor() { - this.store = { 'xpack.uptime.detailPage.selectedView': 'list' }; - } - - clear() { - this.store = {}; - } - - getItem(key: string) { - return this.store[key] || null; - } - - setItem(key: string, value: string) { - this.store[key] = value.toString(); - } - - removeItem(key: string) { - delete this.store[key]; - } -} // Note For shallow test, we need absolute time strings describe('LocationAvailability component', () => { let monitorLocations: MonitorLocations; beforeEach(() => { - // @ts-ignore replacing a call to localStorage we use for monitor list size - global.localStorage = new LocalStorageMock(); - - // @ts-ignore replacing a call to localStorage we use for monitor list size - global.localStorage.setItem('xpack.uptime.detailPage.selectedView', 'list'); - monitorLocations = { monitorId: 'wapo', up_history: 12, @@ -58,106 +29,34 @@ describe('LocationAvailability component', () => { down_history: 0, }, { - summary: { up: 4, down: 0 }, + summary: { up: 2, down: 2 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, + up_history: 2, + down_history: 2, }, { - summary: { up: 4, down: 0 }, + summary: { up: 0, down: 4 }, geo: { name: 'Unnamed-location' }, timestamp: '2020-01-13T22:50:02.753Z', - up_history: 4, - down_history: 0, + up_history: 0, + down_history: 4, }, ], }; }); - it('renders correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('shows warning if geo information is missing', () => { - // @ts-ignore replacing a call to localStorage we use for monitor list size - global.localStorage.setItem('xpack.uptime.detailPage.selectedView', 'map'); - - monitorLocations = { - monitorId: 'wapo', - up_history: 8, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, - geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, - }, - { - summary: { up: 4, down: 0 }, - geo: { name: 'Unnamed-location' }, - timestamp: '2020-01-13T22:50:02.753Z', - up_history: 4, - down_history: 0, - }, - ], - }; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - - const warningComponent = component.find(LocationMissingWarning); - expect(warningComponent).toHaveLength(1); - }); - - it('doesnt shows warning if geo is provided', () => { - monitorLocations = { - monitorId: 'wapo', - up_history: 8, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, - geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } }, - timestamp: '2020-01-13T22:50:06.536Z', - up_history: 4, - down_history: 0, - }, - { - summary: { up: 4, down: 0 }, - geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: '2020-01-13T22:50:04.354Z', - up_history: 4, - down_history: 0, - }, - ], - }; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - - const warningComponent = component.find(LocationMissingWarning); - expect(warningComponent).toHaveLength(0); - }); - - it('renders named locations that have missing geo data', () => { - monitorLocations = { - monitorId: 'wapo', - up_history: 4, - down_history: 0, - locations: [ - { - summary: { up: 4, down: 0 }, - geo: { name: 'New York', location: undefined }, - timestamp: '2020-01-13T22:50:06.536Z', - up_history: 4, - down_history: 0, - }, - ], - }; - - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); + it('renders correctly', () => { + render(); + expect(screen.getByRole('heading', { name: 'Monitoring from', level: 3 })); + expect(screen.getByText('New York')).toBeInTheDocument(); + expect(screen.getByText('Tokyo')).toBeInTheDocument(); + expect(screen.getByText('Unnamed-location')).toBeInTheDocument(); + expect(screen.getByText('100.00 %')).toBeInTheDocument(); + expect(screen.getByText('50.00 %')).toBeInTheDocument(); + expect(screen.getByText('0.00 %')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:06 PM')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:04 PM')).toBeInTheDocument(); + expect(screen.getByText('Jan 13, 2020 5:50:02 PM')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx index 5f74098e12583..c851369d63e9e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/location_availability.tsx @@ -5,18 +5,12 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiTitle } from '@elastic/eui'; import { LocationStatusTags } from '../availability_reporting'; -import { LocationPoint } from '../location_map/embeddables/embedded_map'; -import { MonitorLocations, MonitorLocation } from '../../../../../common/runtime_types'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; -import { LocationMissingWarning } from '../location_map/location_missing'; -import { useSelectedView } from './use_selected_view'; -import { LocationMap } from '../location_map'; +import { MonitorLocations } from '../../../../../common/runtime_types'; import { MonitoringFrom } from '../translations'; -import { ToggleViewBtn } from './toggle_view_btn'; const EuiFlexItemTags = styled(EuiFlexItem)` width: 350px; @@ -30,61 +24,20 @@ interface LocationMapProps { } export const LocationAvailability = ({ monitorLocations }: LocationMapProps) => { - const upPoints: LocationPoint[] = []; - const downPoints: LocationPoint[] = []; - - let isAnyGeoInfoMissing = false; - - if (monitorLocations?.locations) { - monitorLocations.locations.forEach(({ geo, summary }: MonitorLocation) => { - if (geo?.name === UNNAMED_LOCATION || !geo?.location) { - isAnyGeoInfoMissing = true; - } else if (!!geo.location.lat && !!geo.location.lon) { - if (summary?.down === 0) { - upPoints.push(geo as LocationPoint); - } else { - downPoints.push(geo as LocationPoint); - } - } - }); - } - const { selectedView: initialView } = useSelectedView(); - - const [selectedView, setSelectedView] = useState(initialView); - return ( - {selectedView === 'list' && ( - - -

{MonitoringFrom}

-
-
- )} - {selectedView === 'map' && ( - {isAnyGeoInfoMissing && } - )} - - { - setSelectedView(val); - }} - /> + + +

{MonitoringFrom}

+
- {selectedView === 'list' && ( - - - - )} - {selectedView === 'map' && ( - - - - )} + + +
); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx deleted file mode 100644 index 45cb5c45bf021..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/toggle_view_btn.tsx +++ /dev/null @@ -1,66 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import styled from 'styled-components'; -import { EuiButtonGroup } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSelectedView } from './use_selected_view'; -import { ChangeToListView, ChangeToMapView } from '../translations'; - -const ToggleViewButtons = styled.span` - margin-left: auto; -`; - -interface Props { - onChange: (val: string) => void; -} - -export const ToggleViewBtn = ({ onChange }: Props) => { - const toggleButtons = [ - { - id: `listBtn`, - label: ChangeToMapView, - name: 'listView', - iconType: 'list', - 'data-test-subj': 'uptimeMonitorToggleListBtn', - 'aria-label': ChangeToMapView, - }, - { - id: `mapBtn`, - label: ChangeToListView, - name: 'mapView', - iconType: 'mapMarker', - 'data-test-subj': 'uptimeMonitorToggleMapBtn', - 'aria-label': ChangeToListView, - }, - ]; - - const { selectedView, setSelectedView } = useSelectedView(); - - const onChangeView = (optionId: string) => { - const currView = optionId === 'listBtn' ? 'list' : 'map'; - setSelectedView(currView); - onChange(currView); - }; - - return ( - - onChangeView(id)} - type="multi" - isIconOnly - style={{ marginLeft: 'auto' }} - legend={i18n.translate('xpack.uptime.locationAvailabilityViewToggleLegend', { - defaultMessage: 'View toggle', - })} - /> - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts deleted file mode 100644 index fa77d0bf9057e..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_availability/use_selected_view.ts +++ /dev/null @@ -1,27 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; - -const localKey = 'xpack.uptime.detailPage.selectedView'; - -interface Props { - selectedView: string; - setSelectedView: (val: string) => void; -} - -export const useSelectedView = (): Props => { - const getSelectedView = localStorage.getItem(localKey) ?? 'list'; - - const [selectedView, setSelectedView] = useState(getSelectedView); - - useEffect(() => { - localStorage.setItem(localKey, selectedView); - }, [selectedView]); - - return { selectedView, setSelectedView }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap deleted file mode 100644 index 6b3d157c23fee..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_map.test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationMap component renders correctly against snapshot 1`] = ` - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap deleted file mode 100644 index 5e3e2e1a6db46..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/__snapshots__/location_missing.test.tsx.snap +++ /dev/null @@ -1,123 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LocationMissingWarning component renders correctly against snapshot 1`] = ` -.c0 { - margin-left: auto; - margin-bottom: 3px; - margin-right: 5px; -} - -
-
-
-
- -
-
-
-
-`; - -exports[`LocationMissingWarning component shallow render correctly against snapshot 1`] = ` - - - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - - - observer.geo.?? - , - } - } - /> - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts deleted file mode 100644 index b925697970a57..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/__mocks__/poly_layer_mock.ts +++ /dev/null @@ -1,192 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import lowPolyLayerFeatures from '../low_poly_layer.json'; - -export const mockDownPointsLayer = { - id: 'down_points', - label: 'Down Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features: [ - { - id: 'Asia', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 52.487239], - }, - }, - { - id: 'APJ', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 55.487239], - }, - }, - { - id: 'Canada', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [14.399262, 54.487239], - }, - }, - ], - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#BC261E', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', -}; - -export const mockUpPointsLayer = { - id: 'up_points', - label: 'Up Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features: [ - { - id: 'US-EAST', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 52.487239], - }, - }, - { - id: 'US-WEST', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [13.399262, 55.487239], - }, - }, - { - id: 'Europe', - type: 'feature', - geometry: { - type: 'Point', - coordinates: [14.399262, 54.487239], - }, - }, - ], - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', -}; - -export const mockLayerList = [ - { - id: 'low_poly_layer', - label: 'World countries', - minZoom: 0, - maxZoom: 24, - alpha: 1, - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', - type: 'GEOJSON_FILE', - __featureCollection: lowPolyLayerFeatures, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#cad3e4', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 0, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }, - mockDownPointsLayer, - mockUpPointsLayer, -]; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx deleted file mode 100644 index 2dc15127529d9..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx +++ /dev/null @@ -1,175 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState, useContext, useRef } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; -import { - MapEmbeddable, - MapEmbeddableInput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../maps/public/embeddable'; -import * as i18n from './translations'; -import { GeoPoint } from '../../../../../../common/runtime_types'; -import { getLayerList } from './map_config'; -import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../../contexts'; - -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public'; -import { MapToolTipComponent } from './map_tool_tip'; -import { - isErrorEmbeddable, - ViewMode, - ErrorEmbeddable, -} from '../../../../../../../../../src/plugins/embeddable/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; - -export interface EmbeddedMapProps { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export type LocationPoint = Required; - -const EmbeddedPanel = styled.div` - z-index: auto; - flex: 1; - display: flex; - flex-direction: column; - height: 100%; - position: relative; - .embPanel__content { - display: flex; - flex: 1 1 100%; - z-index: 1; - min-height: 0; // Absolute must for Firefox to scroll contents - } - &&& .mapboxgl-canvas { - animation: none !important; - } -`; - -export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { - const { colors } = useContext(UptimeThemeContext); - const [embeddable, setEmbeddable] = useState(); - const embeddableRoot: React.RefObject = useRef(null); - const { embeddable: embeddablePlugin } = useContext(UptimeStartupPluginsContext); - if (!embeddablePlugin) { - throw new Error('Embeddable start plugin not found'); - } - const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); - - const portalNode = React.useMemo(() => createPortalNode(), []); - - const input: MapEmbeddableInput = { - id: uuid.v4(), - attributes: { title: '' }, - filters: [], - hidePanelTitles: true, - refreshConfig: { - value: 0, - pause: false, - }, - viewMode: ViewMode.VIEW, - isLayerTOCOpen: false, - hideFilterActions: true, - // Zoom Lat/Lon values are set to make sure map is in center in the panel - // It wil also omit Greenland/Antarctica etc - mapCenter: { - lon: 11, - lat: 20, - zoom: 0, - }, - mapSettings: { - disableInteractive: true, - hideToolbarOverlay: true, - hideLayerControl: true, - hideViewControl: true, - }, - }; - - const renderTooltipContent = ({ - addFilters, - closeTooltip, - features, - isLocked, - getLayerName, - loadFeatureProperties, - loadFeatureGeometry, - }: RenderTooltipContentParams) => { - const props = { - addFilters, - closeTooltip, - isLocked, - getLayerName, - loadFeatureProperties, - loadFeatureGeometry, - }; - const relevantFeatures = features.filter( - (item: any) => item.layerId === 'up_points' || item.layerId === 'down_points' - ); - if (relevantFeatures.length > 0) { - return ; - } - closeTooltip(); - return null; - }; - - useEffect(() => { - async function setupEmbeddable() { - if (!factory) { - throw new Error('Map embeddable not found.'); - } - const embeddableObject: any = await factory.create({ - ...input, - title: i18n.MAP_TITLE, - }); - - if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - embeddableObject.setRenderTooltipContent(renderTooltipContent); - embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - - setEmbeddable(embeddableObject); - } - - setupEmbeddable(); - - // we want this effect to execute exactly once after the component mounts - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // update map layers based on points - useEffect(() => { - if (embeddable && !isErrorEmbeddable(embeddable)) { - embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - }, [upPoints, downPoints, embeddable, colors]); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot]); - - return ( - -
- - - - - ); -}); - -EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json deleted file mode 100644 index 7a309cd01ebc7..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json +++ /dev/null @@ -1,2898 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "34.21666", - "31.32333" - ], - [ - "35.98361", - "34.52750" - ], - [ - "34.65943", - "36.80527" - ], - [ - "32.77166", - "36.02888" - ], - [ - "29.67722", - "36.11833" - ], - [ - "27.25500", - "36.96500" - ], - [ - "27.51166", - "40.30555" - ], - [ - "33.33860", - "42.01985" - ], - [ - "38.35582", - "40.91027" - ], - [ - "41.77609", - "41.84193" - ], - [ - "41.59748", - "43.22151" - ], - [ - "45.16512", - "42.70333" - ], - [ - "47.91547", - "41.22499" - ], - [ - "49.76062", - "42.71076" - ], - [ - "49.44831", - "45.53038" - ], - [ - "47.30249", - "50.03194" - ], - [ - "52.34180", - "51.78075" - ], - [ - "55.69249", - "50.53249" - ], - [ - "58.33777", - "51.15610" - ], - [ - "57.97027", - "54.38819" - ], - [ - "59.64166", - "55.55867" - ], - [ - "57.22169", - "56.85096" - ], - [ - "59.44912", - "58.48804" - ], - [ - "59.57756", - "63.93287" - ], - [ - "66.10887", - "67.48123" - ], - [ - "64.52222", - "68.90305" - ], - [ - "67.05498", - "68.85637" - ], - [ - "69.32735", - "72.94540" - ], - [ - "73.52553", - "71.81582" - ], - [ - "80.82610", - "72.08693" - ], - [ - "80.51860", - "73.57346" - ], - [ - "89.25278", - "75.50305" - ], - [ - "97.18359", - "75.92804" - ], - [ - "104.07138", - "77.73221" - ], - [ - "111.10387", - "76.75526" - ], - [ - "113.47054", - "73.50096" - ], - [ - "118.63443", - "73.57166" - ], - [ - "131.53580", - "70.87776" - ], - [ - "137.45190", - "71.34109" - ], - [ - "141.02414", - "72.58582" - ], - [ - "149.18524", - "72.22249" - ], - [ - "152.53830", - "70.83777" - ], - [ - "159.72968", - "69.83472" - ], - [ - "170.61194", - "68.75633" - ], - [ - "170.47189", - "70.13416" - ], - [ - "180.00000", - "68.98010" - ], - [ - "180.00000", - "65.06891" - ], - [ - "179.55373", - "62.61971" - ], - [ - "173.54178", - "61.74430" - ], - [ - "170.64194", - "60.41750" - ], - [ - "163.36023", - "59.82388" - ], - [ - "161.93858", - "58.06763" - ], - [ - "163.34996", - "56.19596" - ], - [ - "156.74524", - "51.07791" - ], - [ - "155.54413", - "55.30360" - ], - [ - "155.94206", - "56.65353" - ], - [ - "161.91248", - "60.41972" - ], - [ - "159.24747", - "61.92222" - ], - [ - "152.35718", - "59.02332" - ], - [ - "143.21109", - "59.37666" - ], - [ - "137.72580", - "56.17500" - ], - [ - "137.29327", - "54.07500" - ], - [ - "141.41483", - "53.29361" - ], - [ - "140.17609", - "48.45013" - ], - [ - "135.42233", - "43.75611" - ], - [ - "133.15485", - "42.68263" - ], - [ - "131.81052", - "43.32555" - ], - [ - "129.70204", - "40.83069" - ], - [ - "127.51763", - "39.73957" - ], - [ - "129.42944", - "37.05986" - ], - [ - "129.23749", - "35.18990" - ], - [ - "126.37556", - "34.79138" - ], - [ - "126.38860", - "37.88721" - ], - [ - "124.32395", - "39.91589" - ], - [ - "121.64804", - "38.99638" - ], - [ - "121.17747", - "40.92194" - ], - [ - "118.11053", - "38.14639" - ], - [ - "120.82054", - "36.64527" - ], - [ - "120.24873", - "34.31145" - ], - [ - "121.84693", - "30.85305" - ], - [ - "120.93526", - "27.98222" - ], - [ - "119.58074", - "25.67996" - ], - [ - "116.48172", - "22.93902" - ], - [ - "112.28194", - "21.70139" - ], - [ - "107.36693", - "21.26527" - ], - [ - "105.63857", - "18.89065" - ], - [ - "108.82916", - "15.42194" - ], - [ - "109.46186", - "12.86097" - ], - [ - "109.02168", - "11.36225" - ], - [ - "104.79893", - "8.79222" - ], - [ - "104.98177", - "10.10444" - ], - [ - "100.97635", - "13.46281" - ], - [ - "99.15082", - "10.36472" - ], - [ - "100.57809", - "7.22014" - ], - [ - "103.18192", - "5.28278" - ], - [ - "103.37455", - "1.53347" - ], - [ - "101.28574", - "2.84354" - ], - [ - "100.35553", - "5.96389" - ], - [ - "98.27415", - "8.27444" - ], - [ - "98.74720", - "11.67486" - ], - [ - "97.72457", - "15.84666" - ], - [ - "95.42859", - "15.72972" - ], - [ - "93.72436", - "19.93243" - ], - [ - "91.70444", - "22.48055" - ], - [ - "86.96332", - "21.38194" - ], - [ - "86.42123", - "19.98493" - ], - [ - "80.27943", - "15.69917" - ], - [ - "79.85811", - "10.28583" - ], - [ - "76.99860", - "8.36527" - ], - [ - "74.85526", - "12.75500" - ], - [ - "73.44748", - "16.05861" - ], - [ - "72.56485", - "21.37506" - ], - [ - "70.82513", - "20.69597" - ], - [ - "66.50005", - "25.40381" - ], - [ - "61.76083", - "25.03208" - ], - [ - "57.31909", - "25.77146" - ], - [ - "56.80888", - "27.12361" - ], - [ - "54.78846", - "26.49041" - ], - [ - "51.43027", - "27.93777" - ], - [ - "50.63916", - "29.47042" - ], - [ - "47.95943", - "30.03305" - ], - [ - "48.83887", - "27.61972" - ], - [ - "51.28236", - "24.30000" - ], - [ - "53.58777", - "24.04417" - ], - [ - "55.85944", - "25.72042" - ], - [ - "57.17131", - "23.93444" - ], - [ - "59.82861", - "22.29166" - ], - [ - "57.80569", - "18.97097" - ], - [ - "55.03194", - "17.01472" - ], - [ - "52.18916", - "15.60528" - ], - [ - "45.04232", - "12.75239" - ], - [ - "43.47888", - "12.67500" - ], - [ - "42.78933", - "16.46083" - ], - [ - "40.75694", - "19.76417" - ], - [ - "39.17486", - "21.10402" - ], - [ - "39.06277", - "22.58333" - ], - [ - "35.16055", - "28.05666" - ], - [ - "34.21666", - "31.32333" - ] - ] - ], - [ - [ - [ - "-169.69496", - "66.06806" - ], - [ - "-173.67308", - "64.34679" - ], - [ - "-179.32083", - "65.53012" - ], - [ - "-180.00000", - "65.06891" - ], - [ - "-180.00000", - "68.98010" - ], - [ - "-169.69496", - "66.06806" - ] - ] - ], - [ - [ - [ - "139.93851", - "40.42860" - ], - [ - "142.06970", - "39.54666" - ], - [ - "140.95358", - "38.14805" - ], - [ - "140.33218", - "35.12985" - ], - [ - "137.02879", - "34.56784" - ], - [ - "136.71246", - "36.75139" - ], - [ - "139.42622", - "38.15458" - ], - [ - "139.93851", - "40.42860" - ] - ] - ], - [ - [ - [ - "119.89259", - "15.80112" - ], - [ - "120.58527", - "18.51139" - ], - [ - "122.51833", - "17.04389" - ], - [ - "121.38026", - "15.30250" - ], - [ - "119.89259", - "15.80112" - ] - ] - ], - [ - [ - [ - "122.32916", - "7.30833" - ], - [ - "126.18610", - "9.24277" - ], - [ - "125.37762", - "6.72361" - ], - [ - "123.45888", - "7.81055" - ], - [ - "122.32916", - "7.30833" - ] - ] - ], - [ - [ - [ - "111.89638", - "-3.57389" - ], - [ - "110.23193", - "-2.97111" - ], - [ - "108.84549", - "0.81056" - ], - [ - "109.64857", - "2.07341" - ], - [ - "113.01054", - "3.16055" - ], - [ - "115.37886", - "4.91167" - ], - [ - "116.75417", - "7.01805" - ], - [ - "119.27582", - "5.34500" - ], - [ - "117.27540", - "3.22000" - ], - [ - "117.87192", - "1.87667" - ], - [ - "117.44479", - "-0.52397" - ], - [ - "115.96624", - "-3.60875" - ], - [ - "113.03471", - "-2.98972" - ], - [ - "111.89638", - "-3.57389" - ] - ] - ], - [ - [ - [ - "102.97601", - "0.64348" - ], - [ - "103.36081", - "-0.70222" - ], - [ - "106.05525", - "-3.03139" - ], - [ - "105.72887", - "-5.89826" - ], - [ - "102.32610", - "-4.00611" - ], - [ - "100.90555", - "-2.31944" - ], - [ - "98.70383", - "1.55979" - ], - [ - "95.53108", - "4.68278" - ], - [ - "97.51483", - "5.24944" - ], - [ - "100.41219", - "2.29306" - ], - [ - "102.97601", - "0.64348" - ] - ] - ], - [ - [ - [ - "120.82723", - "1.23406" - ], - [ - "120.01999", - "-0.07528" - ], - [ - "122.47623", - "-3.16090" - ], - [ - "120.32888", - "-5.51208" - ], - [ - "119.35491", - "-5.40007" - ], - [ - "118.88860", - "-2.89319" - ], - [ - "119.77805", - "0.22972" - ], - [ - "120.82723", - "1.23406" - ] - ] - ], - [ - [ - [ - "136.04913", - "-2.69806" - ], - [ - "137.87579", - "-1.47306" - ], - [ - "144.51373", - "-3.82222" - ], - [ - "145.76639", - "-5.48528" - ], - [ - "147.46661", - "-5.97086" - ], - [ - "146.08969", - "-8.09111" - ], - [ - "144.21738", - "-7.79465" - ], - [ - "143.36510", - "-9.01222" - ], - [ - "141.11996", - "-9.23097" - ], - [ - "139.09454", - "-7.56181" - ], - [ - "138.06525", - "-5.40896" - ], - [ - "135.20468", - "-4.45972" - ], - [ - "132.72275", - "-2.81722" - ], - [ - "131.25555", - "-0.82278" - ], - [ - "134.02950", - "-0.96694" - ], - [ - "134.99495", - "-3.33653" - ], - [ - "136.04913", - "-2.69806" - ] - ] - ], - [ - [ - [ - "110.05640", - "-7.89751" - ], - [ - "106.56721", - "-7.41694" - ], - [ - "106.07582", - "-5.88194" - ], - [ - "110.39360", - "-6.97903" - ], - [ - "110.05640", - "-7.89751" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Asia" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "-25.28167", - "71.39166" - ], - [ - "-23.56056", - "70.10609" - ], - [ - "-26.36333", - "68.66748" - ], - [ - "-31.99916", - "68.09526" - ], - [ - "-34.71999", - "66.33832" - ], - [ - "-41.15541", - "64.96235" - ], - [ - "-43.08722", - "60.10027" - ], - [ - "-47.68986", - "61.00680" - ], - [ - "-50.31562", - "62.49430" - ], - [ - "-53.23333", - "65.68283" - ], - [ - "-53.62778", - "67.81470" - ], - [ - "-50.58930", - "69.92373" - ], - [ - "-54.68694", - "72.36721" - ], - [ - "-58.15958", - "75.50860" - ], - [ - "-68.50056", - "76.08693" - ], - [ - "-72.55222", - "78.52110" - ], - [ - "-60.80666", - "81.87997" - ], - [ - "-30.38833", - "83.60220" - ], - [ - "-16.00500", - "80.72859" - ], - [ - "-22.03695", - "77.68568" - ], - [ - "-19.33681", - "75.40207" - ], - [ - "-24.46305", - "73.53581" - ], - [ - "-25.28167", - "71.39166" - ] - ] - ], - [ - [ - [ - "-87.64890", - "76.33804" - ], - [ - "-86.47916", - "79.76167" - ], - [ - "-90.43666", - "81.88750" - ], - [ - "-70.26001", - "83.11388" - ], - [ - "-61.07639", - "82.32083" - ], - [ - "-78.78194", - "76.57221" - ], - [ - "-87.64890", - "76.33804" - ] - ] - ], - [ - [ - [ - "-123.83389", - "73.70027" - ], - [ - "-115.31903", - "73.47707" - ], - [ - "-123.29306", - "71.14610" - ], - [ - "-123.83389", - "73.70027" - ] - ] - ], - [ - [ - [ - "-65.32806", - "62.66610" - ], - [ - "-68.61583", - "62.26389" - ], - [ - "-77.33667", - "65.17609" - ], - [ - "-72.25835", - "67.24803" - ], - [ - "-77.30506", - "69.83395" - ], - [ - "-85.87465", - "70.07943" - ], - [ - "-89.90348", - "71.35304" - ], - [ - "-89.03958", - "73.25499" - ], - [ - "-81.57251", - "73.71971" - ], - [ - "-67.21986", - "69.94081" - ], - [ - "-67.23819", - "68.35790" - ], - [ - "-61.26458", - "66.62609" - ], - [ - "-65.56204", - "64.73154" - ], - [ - "-65.32806", - "62.66610" - ] - ] - ], - [ - [ - [ - "-105.02444", - "72.21999" - ], - [ - "-100.99973", - "70.17276" - ], - [ - "-101.85139", - "68.98442" - ], - [ - "-113.04173", - "68.49374" - ], - [ - "-116.53221", - "69.40887" - ], - [ - "-119.13445", - "71.77457" - ], - [ - "-114.66666", - "73.37247" - ], - [ - "-105.02444", - "72.21999" - ] - ] - ], - [ - [ - [ - "-77.36667", - "8.67500" - ], - [ - "-77.88972", - "7.22889" - ], - [ - "-79.69778", - "8.86666" - ], - [ - "-81.73862", - "8.16250" - ], - [ - "-85.65668", - "9.90500" - ], - [ - "-85.66959", - "11.05500" - ], - [ - "-87.93779", - "13.15639" - ], - [ - "-91.38474", - "13.97889" - ], - [ - "-93.93861", - "16.09389" - ], - [ - "-96.47612", - "15.64361" - ], - [ - "-103.45001", - "18.31361" - ], - [ - "-105.67834", - "20.38305" - ], - [ - "-105.18945", - "21.43750" - ], - [ - "-106.91570", - "23.86514" - ], - [ - "-109.43750", - "25.82027" - ], - [ - "-109.44431", - "26.71555" - ], - [ - "-112.16195", - "28.97139" - ], - [ - "-113.09167", - "31.22972" - ], - [ - "-115.69667", - "29.77423" - ], - [ - "-117.40944", - "33.24416" - ], - [ - "-120.60583", - "34.55860" - ], - [ - "-124.33118", - "40.27246" - ], - [ - "-124.52444", - "42.86610" - ], - [ - "-123.87161", - "45.52898" - ], - [ - "-124.71431", - "48.39708" - ], - [ - "-124.03510", - "49.91801" - ], - [ - "-127.17315", - "50.92221" - ], - [ - "-130.88640", - "55.70791" - ], - [ - "-133.81302", - "57.97293" - ], - [ - "-136.65891", - "58.21652" - ], - [ - "-140.40335", - "59.69804" - ], - [ - "-146.75543", - "60.95249" - ], - [ - "-154.23567", - "58.13069" - ], - [ - "-157.55139", - "58.38777" - ], - [ - "-165.42244", - "60.55215" - ], - [ - "-164.40112", - "63.21499" - ], - [ - "-168.13196", - "65.66296" - ], - [ - "-161.66779", - "67.02054" - ], - [ - "-166.82362", - "68.34873" - ], - [ - "-156.59673", - "71.35144" - ], - [ - "-151.22986", - "70.37296" - ], - [ - "-143.21555", - "70.11026" - ], - [ - "-137.25500", - "68.94832" - ], - [ - "-127.18096", - "70.27638" - ], - [ - "-114.06652", - "68.46970" - ], - [ - "-112.39584", - "67.67915" - ], - [ - "-98.11124", - "67.83887" - ], - [ - "-90.43639", - "68.87442" - ], - [ - "-85.55499", - "69.85970" - ], - [ - "-81.33570", - "69.18498" - ], - [ - "-81.50222", - "67.00096" - ], - [ - "-85.89726", - "66.16802" - ], - [ - "-87.98736", - "64.18845" - ], - [ - "-92.71001", - "62.46583" - ], - [ - "-94.78972", - "59.09222" - ], - [ - "-92.41875", - "57.33270" - ], - [ - "-88.81500", - "56.82444" - ], - [ - "-85.00195", - "55.29666" - ], - [ - "-82.30777", - "55.14888" - ], - [ - "-82.27390", - "52.95638" - ], - [ - "-78.57945", - "52.11138" - ], - [ - "-79.76181", - "54.65166" - ], - [ - "-76.67979", - "56.03645" - ], - [ - "-78.57299", - "58.62888" - ], - [ - "-77.50835", - "62.56166" - ], - [ - "-73.68346", - "62.47999" - ], - [ - "-70.14848", - "61.08458" - ], - [ - "-67.56610", - "58.22360" - ], - [ - "-64.74538", - "60.23075" - ], - [ - "-61.09055", - "55.84415" - ], - [ - "-57.34969", - "54.57496" - ], - [ - "-56.95160", - "51.42458" - ], - [ - "-60.00500", - "50.24888" - ], - [ - "-66.44903", - "50.26777" - ], - [ - "-64.21167", - "48.88499" - ], - [ - "-64.90430", - "46.84597" - ], - [ - "-63.66708", - "45.81666" - ], - [ - "-70.19187", - "43.57555" - ], - [ - "-70.72610", - "41.72777" - ], - [ - "-74.13390", - "40.70082" - ], - [ - "-75.96083", - "37.15221" - ], - [ - "-76.34326", - "34.88194" - ], - [ - "-78.82750", - "33.73027" - ], - [ - "-81.48843", - "31.11347" - ], - [ - "-80.03534", - "26.79569" - ], - [ - "-81.73659", - "25.95944" - ], - [ - "-84.01098", - "30.09764" - ], - [ - "-88.98083", - "30.41833" - ], - [ - "-94.75417", - "29.36791" - ], - [ - "-97.56041", - "26.84208" - ], - [ - "-97.74223", - "22.01250" - ], - [ - "-95.80112", - "18.74500" - ], - [ - "-94.46918", - "18.14625" - ], - [ - "-90.73167", - "19.36153" - ], - [ - "-90.27972", - "21.06305" - ], - [ - "-86.82973", - "21.42923" - ], - [ - "-88.28250", - "17.62389" - ], - [ - "-88.13696", - "15.68285" - ], - [ - "-84.26015", - "15.82597" - ], - [ - "-83.18695", - "14.32389" - ], - [ - "-83.84751", - "11.17458" - ], - [ - "-82.24278", - "9.00236" - ], - [ - "-79.53445", - "9.62014" - ], - [ - "-77.36667", - "8.67500" - ] - ] - ], - [ - [ - [ - "-55.19333", - "46.98499" - ], - [ - "-59.40361", - "47.89423" - ], - [ - "-56.68250", - "51.33943" - ], - [ - "-55.56114", - "49.36818" - ], - [ - "-52.83465", - "48.09965" - ], - [ - "-55.19333", - "46.98499" - ] - ] - ], - [ - [ - [ - "-73.03644", - "18.45622" - ], - [ - "-72.79834", - "19.94278" - ], - [ - "-69.94932", - "19.67680" - ], - [ - "-68.89528", - "18.39639" - ], - [ - "-73.03644", - "18.45622" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "North America" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "64.52222", - "68.90305" - ], - [ - "66.10887", - "67.48123" - ], - [ - "59.57756", - "63.93287" - ], - [ - "59.44912", - "58.48804" - ], - [ - "57.22169", - "56.85096" - ], - [ - "59.64166", - "55.55867" - ], - [ - "57.97027", - "54.38819" - ], - [ - "58.33777", - "51.15610" - ], - [ - "55.69249", - "50.53249" - ], - [ - "52.34180", - "51.78075" - ], - [ - "47.30249", - "50.03194" - ], - [ - "49.44831", - "45.53038" - ], - [ - "49.76062", - "42.71076" - ], - [ - "47.91547", - "41.22499" - ], - [ - "45.16512", - "42.70333" - ], - [ - "41.59748", - "43.22151" - ], - [ - "39.94553", - "43.39693" - ], - [ - "34.70249", - "46.17582" - ], - [ - "30.83277", - "46.54832" - ], - [ - "28.78083", - "44.66096" - ], - [ - "28.01305", - "41.98222" - ], - [ - "26.36041", - "40.95388" - ], - [ - "22.59500", - "40.01221" - ], - [ - "23.96055", - "38.28166" - ], - [ - "22.15246", - "37.01854" - ], - [ - "19.30721", - "40.64531" - ], - [ - "19.59771", - "41.80611" - ], - [ - "15.15167", - "44.19639" - ], - [ - "13.02958", - "41.26014" - ], - [ - "8.74722", - "44.42805" - ], - [ - "6.16528", - "43.05055" - ], - [ - "4.05625", - "43.56277" - ], - [ - "3.20167", - "41.89278" - ], - [ - "0.99306", - "41.04805" - ], - [ - "0.20722", - "38.73221" - ], - [ - "-2.12292", - "36.73347" - ], - [ - "-5.61361", - "36.00610" - ], - [ - "-6.95992", - "37.22184" - ], - [ - "-8.98924", - "37.02631" - ], - [ - "-9.49083", - "38.79388" - ], - [ - "-8.66014", - "40.69111" - ], - [ - "-9.16972", - "43.18583" - ], - [ - "-1.44389", - "43.64055" - ], - [ - "-1.11463", - "46.31658" - ], - [ - "-2.68528", - "48.50166" - ], - [ - "1.43875", - "50.10083" - ], - [ - "5.59917", - "53.30028" - ], - [ - "13.80854", - "53.85479" - ], - [ - "21.24506", - "54.95506" - ], - [ - "21.05223", - "56.81749" - ], - [ - "23.43159", - "59.95382" - ], - [ - "21.42416", - "60.57930" - ], - [ - "21.58500", - "64.43971" - ], - [ - "17.09861", - "61.60278" - ], - [ - "19.07264", - "59.73819" - ], - [ - "16.37982", - "56.66333" - ], - [ - "12.46007", - "56.29666" - ], - [ - "10.51569", - "59.30624" - ], - [ - "8.12750", - "58.09888" - ], - [ - "5.50847", - "58.66764" - ], - [ - "4.94944", - "61.41041" - ], - [ - "9.54528", - "63.76611" - ], - [ - "15.28833", - "68.03055" - ], - [ - "21.30000", - "70.24693" - ], - [ - "28.20778", - "71.07999" - ], - [ - "32.80605", - "69.30277" - ], - [ - "43.75180", - "67.31152" - ], - [ - "53.60437", - "68.90818" - ], - [ - "64.52222", - "68.90305" - ] - ] - ], - [ - [ - [ - "-13.49944", - "65.06915" - ], - [ - "-18.77500", - "63.39139" - ], - [ - "-22.04556", - "64.04666" - ], - [ - "-22.42167", - "66.43332" - ], - [ - "-16.41736", - "66.27603" - ], - [ - "-13.49944", - "65.06915" - ] - ] - ], - [ - [ - [ - "-4.19667", - "57.48583" - ], - [ - "-0.07931", - "54.11340" - ], - [ - "0.25389", - "50.73861" - ], - [ - "-3.43722", - "50.60500" - ], - [ - "-4.19639", - "53.20611" - ], - [ - "-2.89979", - "53.72499" - ], - [ - "-6.22778", - "56.69722" - ], - [ - "-4.19667", - "57.48583" - ] - ] - ], - [ - [ - [ - "12.44167", - "37.80611" - ], - [ - "15.64794", - "38.26458" - ], - [ - "15.08139", - "36.64916" - ], - [ - "12.44167", - "37.80611" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Europe" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "34.21666", - "31.32333" - ], - [ - "34.90380", - "29.48671" - ], - [ - "33.93833", - "26.65528" - ], - [ - "36.88625", - "22.05319" - ], - [ - "37.43569", - "18.85389" - ], - [ - "38.58902", - "18.06680" - ], - [ - "39.71805", - "15.08805" - ], - [ - "41.17222", - "14.63069" - ], - [ - "43.32750", - "12.47673" - ], - [ - "44.27833", - "10.44778" - ], - [ - "50.09319", - "11.51458" - ], - [ - "51.14555", - "10.63361" - ], - [ - "48.00055", - "4.52306" - ], - [ - "46.02555", - "2.43722" - ], - [ - "43.48861", - "0.65000" - ], - [ - "40.12548", - "-3.26569" - ], - [ - "38.77611", - "-6.03972" - ], - [ - "40.38777", - "-11.31778" - ], - [ - "40.57833", - "-15.49889" - ], - [ - "34.89069", - "-19.86042" - ], - [ - "35.45611", - "-24.16945" - ], - [ - "32.81111", - "-25.61209" - ], - [ - "32.39444", - "-28.53139" - ], - [ - "27.90000", - "-33.04056" - ], - [ - "24.82472", - "-34.20167" - ], - [ - "22.53916", - "-34.01118" - ], - [ - "20.00000", - "-34.82200" - ], - [ - "17.84750", - "-32.83083" - ], - [ - "18.21791", - "-31.73458" - ], - [ - "15.09500", - "-26.73528" - ], - [ - "14.51139", - "-22.55278" - ], - [ - "11.76764", - "-17.98820" - ], - [ - "11.73125", - "-15.85070" - ], - [ - "13.84944", - "-10.95611" - ], - [ - "13.39180", - "-8.39375" - ], - [ - "11.77417", - "-4.54264" - ], - [ - "9.70250", - "-2.44792" - ], - [ - "9.29833", - "-0.37167" - ], - [ - "9.96514", - "3.08521" - ], - [ - "8.89861", - "4.58833" - ], - [ - "5.93583", - "4.33833" - ], - [ - "4.41021", - "6.35993" - ], - [ - "1.46889", - "6.18639" - ], - [ - "-2.05889", - "4.73083" - ], - [ - "-4.46806", - "5.29556" - ], - [ - "-7.43639", - "4.34917" - ], - [ - "-9.23889", - "5.12278" - ], - [ - "-12.50417", - "7.38861" - ], - [ - "-13.49313", - "9.56008" - ], - [ - "-15.00542", - "10.77194" - ], - [ - "-17.17556", - "14.65444" - ], - [ - "-16.03945", - "17.73458" - ], - [ - "-16.91625", - "21.94542" - ], - [ - "-12.96271", - "27.92048" - ], - [ - "-11.51195", - "28.30375" - ], - [ - "-9.64097", - "30.16500" - ], - [ - "-8.53833", - "33.25055" - ], - [ - "-6.84306", - "34.01861" - ], - [ - "-5.91874", - "35.79065" - ], - [ - "-1.97972", - "35.07333" - ], - [ - "1.18250", - "36.51221" - ], - [ - "9.85868", - "37.32833" - ], - [ - "11.12667", - "35.24194" - ], - [ - "11.17430", - "33.21006" - ], - [ - "15.16583", - "32.39861" - ], - [ - "15.75430", - "31.38972" - ], - [ - "18.95750", - "30.27639" - ], - [ - "20.56763", - "32.56091" - ], - [ - "29.03500", - "30.82417" - ], - [ - "30.35545", - "31.50284" - ], - [ - "34.21666", - "31.32333" - ] - ] - ], - [ - [ - [ - "48.03140", - "-14.06341" - ], - [ - "49.94333", - "-13.03945" - ], - [ - "50.48277", - "-15.40583" - ], - [ - "49.36833", - "-18.35139" - ], - [ - "47.13305", - "-24.92806" - ], - [ - "44.01708", - "-24.98083" - ], - [ - "43.23888", - "-22.28250" - ], - [ - "44.48277", - "-19.96584" - ], - [ - "43.93139", - "-17.50056" - ], - [ - "44.87360", - "-16.21028" - ], - [ - "48.03140", - "-14.06341" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Africa" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - "-77.88972", - "7.22889" - ], - [ - "-77.36667", - "8.67500" - ], - [ - "-75.63432", - "9.44819" - ], - [ - "-74.86081", - "11.12549" - ], - [ - "-68.84368", - "11.44708" - ], - [ - "-68.11424", - "10.48493" - ], - [ - "-61.87959", - "10.72833" - ], - [ - "-61.61987", - "9.90528" - ], - [ - "-57.51919", - "6.27077" - ], - [ - "-52.97320", - "5.47305" - ], - [ - "-51.25931", - "4.15250" - ], - [ - "-49.90320", - "1.17444" - ], - [ - "-51.92751", - "-1.33486" - ], - [ - "-48.42722", - "-1.66028" - ], - [ - "-47.28556", - "-0.59917" - ], - [ - "-42.23584", - "-2.83778" - ], - [ - "-39.99875", - "-2.84653" - ], - [ - "-37.17445", - "-4.91861" - ], - [ - "-35.47973", - "-5.16611" - ], - [ - "-34.83129", - "-6.98180" - ], - [ - "-35.32751", - "-9.22889" - ], - [ - "-39.05709", - "-13.38028" - ], - [ - "-38.87195", - "-15.87417" - ], - [ - "-39.70403", - "-19.42361" - ], - [ - "-42.03445", - "-22.91917" - ], - [ - "-44.67521", - "-23.05570" - ], - [ - "-48.02612", - "-25.01500" - ], - [ - "-48.84251", - "-28.61778" - ], - [ - "-52.21764", - "-31.74500" - ], - [ - "-54.14077", - "-34.66466" - ], - [ - "-56.15834", - "-34.92722" - ], - [ - "-56.67834", - "-36.92361" - ], - [ - "-58.30112", - "-38.48500" - ], - [ - "-62.06875", - "-39.50848" - ], - [ - "-62.39001", - "-40.90195" - ], - [ - "-65.13014", - "-40.84417" - ], - [ - "-65.24945", - "-44.31306" - ], - [ - "-67.58435", - "-46.00030" - ], - [ - "-65.78979", - "-47.96584" - ], - [ - "-68.94112", - "-50.38806" - ], - [ - "-68.99014", - "-51.62445" - ], - [ - "-72.11501", - "-53.68764" - ], - [ - "-74.28924", - "-50.48049" - ], - [ - "-74.74139", - "-47.71146" - ], - [ - "-72.61389", - "-44.47278" - ], - [ - "-73.99432", - "-40.96695" - ], - [ - "-73.22404", - "-39.41688" - ], - [ - "-73.67709", - "-37.34729" - ], - [ - "-71.44667", - "-32.66500" - ], - [ - "-71.69585", - "-30.50667" - ], - [ - "-70.91389", - "-27.62445" - ], - [ - "-70.05334", - "-21.42565" - ], - [ - "-70.31202", - "-18.43750" - ], - [ - "-71.49424", - "-17.30223" - ], - [ - "-75.05139", - "-15.46597" - ], - [ - "-76.39480", - "-13.88417" - ], - [ - "-78.99459", - "-8.21965" - ], - [ - "-81.17473", - "-6.08667" - ], - [ - "-81.27640", - "-4.28083" - ], - [ - "-79.95632", - "-3.20778" - ], - [ - "-80.91279", - "-1.03653" - ], - [ - "-80.10084", - "0.77028" - ], - [ - "-78.88929", - "1.23837" - ], - [ - "-77.43445", - "4.03139" - ], - [ - "-77.88972", - "7.22889" - ] - ] - ] - }, - "properties": { - "CONTINENT": "South America" - } - }, - { - "type": "Feature", - "geometry": { - "type": "MultiPolygon", - "coordinates": [ - [ - [ - [ - "177.91779", - "-38.94280" - ], - [ - "175.95523", - "-41.25528" - ], - [ - "173.75165", - "-39.27000" - ], - [ - "174.94025", - "-38.10111" - ], - [ - "177.91779", - "-38.94280" - ] - ] - ], - [ - [ - [ - "171.18524", - "-44.93833" - ], - [ - "169.45801", - "-46.62333" - ], - [ - "166.47690", - "-45.80972" - ], - [ - "168.37233", - "-44.04056" - ], - [ - "171.15166", - "-42.56042" - ], - [ - "172.63025", - "-40.51056" - ], - [ - "174.23636", - "-41.83722" - ], - [ - "171.18524", - "-44.93833" - ] - ] - ] - ] - }, - "properties": { - "CONTINENT": "Oceania" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - "151.54025", - "-24.04583" - ], - [ - "153.18192", - "-25.94944" - ], - [ - "153.62419", - "-28.66104" - ], - [ - "152.52969", - "-32.40361" - ], - [ - "151.45456", - "-33.31681" - ], - [ - "149.97163", - "-37.52222" - ], - [ - "146.87357", - "-38.65166" - ], - [ - "143.54295", - "-38.85923" - ], - [ - "140.52997", - "-38.00028" - ], - [ - "138.09225", - "-34.13493" - ], - [ - "135.49586", - "-34.61708" - ], - [ - "134.18414", - "-32.48666" - ], - [ - "131.14859", - "-31.47403" - ], - [ - "125.97227", - "-32.26674" - ], - [ - "123.73499", - "-33.77972" - ], - [ - "120.00499", - "-33.92889" - ], - [ - "117.93414", - "-35.12534" - ], - [ - "115.00895", - "-34.26243" - ], - [ - "115.73998", - "-31.86806" - ], - [ - "113.64346", - "-26.65431" - ], - [ - "113.38971", - "-24.42944" - ], - [ - "114.03027", - "-21.84167" - ], - [ - "116.70749", - "-20.64917" - ], - [ - "121.02748", - "-19.59222" - ], - [ - "122.95623", - "-16.58681" - ], - [ - "126.85790", - "-13.75097" - ], - [ - "129.08942", - "-14.89944" - ], - [ - "130.57927", - "-12.40465" - ], - [ - "132.67198", - "-11.50813" - ], - [ - "135.23135", - "-12.29445" - ], - [ - "135.45135", - "-14.93278" - ], - [ - "136.76581", - "-15.90445" - ], - [ - "140.83330", - "-17.45194" - ], - [ - "141.66553", - "-15.02653" - ], - [ - "141.59412", - "-12.53167" - ], - [ - "142.78830", - "-11.08056" - ], - [ - "143.78220", - "-14.41333" - ], - [ - "145.31580", - "-14.94555" - ], - [ - "146.27762", - "-18.88701" - ], - [ - "147.43192", - "-19.41236" - ], - [ - "150.81912", - "-22.73194" - ], - [ - "151.54025", - "-24.04583" - ] - ] - ] - }, - "properties": { - "CONTINENT": "Australia" - } - } - ] -} \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts deleted file mode 100644 index 5ad92d4e6d1d7..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.test.ts +++ /dev/null @@ -1,47 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getLayerList } from './map_config'; -import { mockLayerList } from './__mocks__/poly_layer_mock'; -import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../../apps/uptime_app'; - -jest.mock('uuid', () => { - return { - v4: jest.fn(() => 'uuid.v4()'), - }; -}); - -describe('map_config', () => { - let upPoints: LocationPoint[]; - let downPoints: LocationPoint[]; - let colors: Pick; - - beforeEach(() => { - upPoints = [ - { name: 'US-EAST', location: { lat: '52.487239', lon: '13.399262' } }, - { location: { lat: '55.487239', lon: '13.399262' }, name: 'US-WEST' }, - { location: { lat: '54.487239', lon: '14.399262' }, name: 'Europe' }, - ]; - downPoints = [ - { location: { lat: '52.487239', lon: '13.399262' }, name: 'Asia' }, - { location: { lat: '55.487239', lon: '13.399262' }, name: 'APJ' }, - { location: { lat: '54.487239', lon: '14.399262' }, name: 'Canada' }, - ]; - colors = { - danger: '#BC261E', - gray: '#000', - }; - }); - - describe('#getLayerList', () => { - test('it returns the low poly layer', () => { - const layerList = getLayerList(upPoints, downPoints, colors); - expect(layerList).toStrictEqual(mockLayerList); - }); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts deleted file mode 100644 index 723eee6f14b80..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_config.ts +++ /dev/null @@ -1,175 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import lowPolyLayerFeatures from './low_poly_layer.json'; -import { LocationPoint } from './embedded_map'; -import { UptimeAppColors } from '../../../../../apps/uptime_app'; - -/** - * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, - * destination, and line layer for each of the provided indexPatterns - * - */ -export const getLayerList = ( - upPoints: LocationPoint[], - downPoints: LocationPoint[], - { danger }: Pick -) => { - return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)]; -}; - -export const getLowPolyLayer = () => { - return { - id: 'low_poly_layer', - label: 'World countries', - minZoom: 0, - maxZoom: 24, - alpha: 1, - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', - type: 'GEOJSON_FILE', - __featureCollection: lowPolyLayerFeatures, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#cad3e4', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 0, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; - -export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: string) => { - const features = downPoints?.map((point) => ({ - type: 'feature', - id: point.name, - geometry: { - type: 'Point', - coordinates: [+point.location.lon, +point.location.lat], - }, - })); - return { - id: 'down_points', - label: 'Down Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features, - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: dangerColor, - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; - -export const getUpPointsLayer = (upPoints: LocationPoint[]) => { - const features = upPoints?.map((point) => ({ - type: 'feature', - id: point.name, - geometry: { - type: 'Point', - coordinates: [+point.location.lon, +point.location.lat], - }, - })); - return { - id: 'up_points', - label: 'Up Locations', - sourceDescriptor: { - type: 'GEOJSON_FILE', - __featureCollection: { - features, - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx deleted file mode 100644 index c03ed94f8c544..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ /dev/null @@ -1,91 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import React, { useContext } from 'react'; -import { useSelector } from 'react-redux'; -import { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiOutsideClickDetector, - EuiPopoverTitle, -} from '@elastic/eui'; -import { TagLabel } from '../../availability_reporting'; -import { UptimeThemeContext } from '../../../../../contexts'; -import { AppState } from '../../../../../state'; -import { monitorLocationsSelector } from '../../../../../state/selectors'; -import { useMonitorId } from '../../../../../hooks'; -import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; -import type { RenderTooltipContentParams } from '../../../../../../../maps/public'; -import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting'; -import { LastCheckLabel } from '../../translations'; - -type MapToolTipProps = Partial; - -export const MapToolTipComponent = ({ closeTooltip, features = [] }: MapToolTipProps) => { - const { id: featureId, layerId } = features[0] ?? {}; - const locationName = featureId?.toString(); - const { - colors: { gray, danger }, - } = useContext(UptimeThemeContext); - - const monitorId = useMonitorId(); - - const monitorLocations = useSelector((state: AppState) => - monitorLocationsSelector(state, monitorId) - ); - if (!locationName || !monitorLocations?.locations) { - return null; - } - const { - timestamp, - up_history: ups, - down_history: downs, - }: MonitorLocation = monitorLocations.locations!.find( - ({ geo }: MonitorLocation) => geo.name === locationName - )!; - - const availability = (ups / (ups + downs)) * 100; - - return ( - { - if (closeTooltip != null) { - closeTooltip(); - } - }} - > - <> - - {layerId === 'up_points' ? ( - - ) : ( - - )} - - - Availability - - {i18n.translate('xpack.uptime.mapToolTip.AvailabilityStat.title', { - defaultMessage: '{value} %', - values: { value: formatAvailabilityValue(availability) }, - description: 'A percentage value like 23.5%', - })} - - {LastCheckLabel} - - {moment(timestamp).fromNow()} - - - - - ); -}; - -export const MapToolTip = React.memo(MapToolTipComponent); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts deleted file mode 100644 index edbf2b5b5e864..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/translations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const MAP_TITLE = i18n.translate( - 'xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle', - { - defaultMessage: 'Monitor Observer Location Map', - } -); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx deleted file mode 100644 index 9818fc164193c..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.test.tsx +++ /dev/null @@ -1,34 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; -import { LocationMap } from './location_map'; -import { LocationPoint } from './embeddables/embedded_map'; - -// Note For shallow test, we need absolute time strings -describe('LocationMap component', () => { - let upPoints: LocationPoint[]; - - beforeEach(() => { - upPoints = [ - { - name: 'New York', - location: { lat: '40.730610', lon: ' -73.935242' }, - }, - { - name: 'Tokyo', - location: { lat: '52.487448', lon: ' 13.394798' }, - }, - ]; - }); - - it('renders correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx deleted file mode 100644 index 5a912a44b7c9a..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_map.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; - -// These height/width values are used to make sure map is in center of panel -// And to make sure, it doesn't take too much space -const MapPanel = styled.div` - height: 240px; - width: 520px; - margin-right: 65px; - @media (max-width: 574px) { - height: 250px; - width: 100%; - } -`; - -interface Props { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export const LocationMap = ({ upPoints, downPoints }: Props) => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx deleted file mode 100644 index dad7a61e74999..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.test.tsx +++ /dev/null @@ -1,22 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { LocationMissingWarning } from './location_missing'; - -describe('LocationMissingWarning component', () => { - it('shallow render correctly against snapshot', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('renders correctly against snapshot', () => { - const component = renderWithIntl(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx deleted file mode 100644 index 7b03f516decad..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/location_missing.tsx +++ /dev/null @@ -1,79 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiSpacer, - EuiText, - EuiCode, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { LocationLink } from '../../../common/location_link'; - -const EuiPopoverRight = styled(EuiFlexItem)` - margin-left: auto; - margin-bottom: 3px; - margin-right: 5px; -`; - -export const LocationMissingWarning = () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const togglePopover = () => { - setIsPopoverOpen(!isPopoverOpen); - }; - - const button = ( - - - - ); - - return ( - - - - - observer.geo.?? }} - /> - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 9376a83f48b3d..23270b1a8dd5b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -30,7 +30,6 @@ export const networkItems: NetworkItems = [ status: 200, mimeType: 'text/css', requestSentTime: 18098833.175, - requestStartTime: 18098835.439, loadEndTime: 18098957.145, timings: { connect: 81.10800000213203, @@ -53,8 +52,8 @@ export const networkItems: NetworkItems = [ }, certificates: { issuer: 'Sample Issuer', - validFrom: 1578441600000, - validTo: 1617883200000, + validFrom: '2021-02-22T18:35:26.000Z', + validTo: '2021-04-05T22:28:43.000Z', subjectName: '*.elastic.co', }, ip: '104.18.8.22', @@ -66,7 +65,6 @@ export const networkItems: NetworkItems = [ status: 200, mimeType: 'application/javascript', requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, loadEndTime: 18098977.648000002, timings: { blocked: 84.54599999822676, @@ -152,7 +150,6 @@ export const networkItemsWithUncommonMimeType: NetworkItems = [ status: 200, mimeType: 'application/x-javascript', requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, loadEndTime: 18098977.648000002, timings: { blocked: 84.54599999822676, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.test.tsx new file mode 100644 index 0000000000000..b35fdb6100826 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.test.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render } from '../../../../../lib/helper/rtl_helpers'; +import { WaterfallChartContainer } from './waterfall_chart_container'; + +const networkEvents = { + events: [ + { + timestamp: '2021-01-21T10:31:21.537Z', + method: 'GET', + url: + 'https://apv-static.minute.ly/videos/v-c2a526c7-450d-428e-1244649-a390-fb639ffead96-s45.746-54.421m.mp4', + status: 206, + mimeType: 'video/mp4', + requestSentTime: 241114127.474, + loadEndTime: 241116573.402, + timings: { + total: 2445.928000001004, + queueing: 1.7399999778717756, + blocked: 0.391999987186864, + receive: 2283.964000031119, + connect: 91.5709999972023, + wait: 28.795999998692423, + proxy: -1, + dns: 36.952000024029985, + send: 0.10000000474974513, + ssl: 64.28900000173599, + }, + }, + { + timestamp: '2021-01-21T10:31:22.174Z', + method: 'GET', + url: 'https://dpm.demdex.net/ibs:dpid=73426&dpuuid=31597189268188866891125449924942215949', + status: 200, + mimeType: 'image/gif', + requestSentTime: 241114749.202, + loadEndTime: 241114805.541, + timings: { + queueing: 1.2240000069141388, + receive: 2.218999987235293, + proxy: -1, + dns: -1, + send: 0.14200000441633165, + blocked: 1.033000007737428, + total: 56.33900000248104, + wait: 51.72099999617785, + ssl: -1, + connect: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.679Z', + method: 'GET', + url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-t119-homepage-mediawall', + status: 200, + mimeType: 'application/json', + requestSentTime: 241114268.04299998, + loadEndTime: 241114665.609, + timings: { + total: 397.5659999996424, + dns: 29.5429999823682, + wait: 221.6830000106711, + queueing: 2.1410000044852495, + connect: 106.95499999565072, + ssl: 69.06899999012239, + receive: 2.027999988058582, + blocked: 0.877000013133511, + send: 23.719999997410923, + proxy: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.runtime.b313577971db9c857801.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114303.84899998, + loadEndTime: 241114370.361, + timings: { + send: 1.357000001007691, + wait: 40.12299998430535, + receive: 16.78500001435168, + ssl: -1, + queueing: 2.5670000177342445, + total: 66.51200001942925, + connect: -1, + blocked: 5.680000002030283, + proxy: -1, + dns: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.modules.7a266e7acfd42f2581a5.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114305.939, + loadEndTime: 241114938.264, + timings: { + wait: 51.61500000394881, + dns: -1, + ssl: -1, + receive: 506.5750000067055, + proxy: -1, + connect: -1, + blocked: 69.51599998865277, + queueing: 4.453999979887158, + total: 632.324999984121, + send: 0.16500000492669642, + }, + }, + ], +}; + +const defaultState = { + networkEvents: { + test: { + '1': { + ...networkEvents, + total: 100, + isWaterfallSupported: true, + loading: false, + }, + }, + }, +}; + +describe('WaterfallChartContainer', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + it('does not display waterfall chart unavailable when isWaterfallSupported is true', () => { + render(, { + state: defaultState, + }); + expect(screen.queryByText('Waterfall chart unavailable')).not.toBeInTheDocument(); + }); + + it('displays waterfall chart unavailable when isWaterfallSupported is false', () => { + const state = { + networkEvents: { + test: { + '1': { + ...networkEvents, + total: 100, + isWaterfallSupported: false, + loading: false, + }, + }, + }, + }; + render(, { + state, + }); + expect(screen.getByText('Waterfall chart unavailable')).toBeInTheDocument(); + }); + + it('displays loading bar when loading', () => { + const state = { + networkEvents: { + test: { + '1': { + ...networkEvents, + total: 100, + isWaterfallSupported: false, + loading: true, + }, + }, + }, + }; + render(, { + state, + }); + expect(screen.getByLabelText('Waterfall chart loading')).toBeInTheDocument(); + }); + + it('displays no data available message when no events are available', () => { + const state = { + networkEvents: { + test: { + '1': { + events: [], + total: 0, + isWaterfallSupported: true, + loading: false, + }, + }, + }, + }; + render(, { + state, + }); + expect(screen.getByText('No waterfall data could be found for this step')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx index 43f822726a4fa..044353125e748 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingChart } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingChart, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { getNetworkEvents } from '../../../../../state/actions/network_events'; @@ -39,18 +40,25 @@ export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex const _networkEvents = useSelector(networkEventsSelector); const networkEvents = _networkEvents[checkGroup ?? '']?.[stepIndex]; + const waterfallLoaded = networkEvents && !networkEvents.loading; + const isWaterfallSupported = networkEvents?.isWaterfallSupported; + const hasEvents = networkEvents?.events?.length > 0; return ( <> - {!networkEvents || - (networkEvents.loading && ( - - - - - - ))} - {networkEvents && !networkEvents.loading && networkEvents.events.length === 0 && ( + {!waterfallLoaded && ( + + + + + + )} + {waterfallLoaded && !hasEvents && ( @@ -59,12 +67,29 @@ export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex )} - {networkEvents && !networkEvents.loading && networkEvents.events.length > 0 && ( + {waterfallLoaded && hasEvents && isWaterfallSupported && ( )} + {waterfallLoaded && hasEvents && !isWaterfallSupported && ( + + } + color="warning" + iconType="help" + > + + + )} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx index 47c18225f38d3..3a0a30980ab52 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -204,7 +204,6 @@ const NETWORK_EVENTS = { status: 206, mimeType: 'video/mp4', requestSentTime: 241114127.474, - requestStartTime: 241114129.214, loadEndTime: 241116573.402, timings: { total: 2445.928000001004, @@ -226,7 +225,6 @@ const NETWORK_EVENTS = { status: 200, mimeType: 'image/gif', requestSentTime: 241114749.202, - requestStartTime: 241114750.426, loadEndTime: 241114805.541, timings: { queueing: 1.2240000069141388, @@ -248,7 +246,6 @@ const NETWORK_EVENTS = { status: 200, mimeType: 'application/json', requestSentTime: 241114268.04299998, - requestStartTime: 241114270.184, loadEndTime: 241114665.609, timings: { total: 397.5659999996424, @@ -270,7 +267,6 @@ const NETWORK_EVENTS = { status: 200, mimeType: 'application/javascript', requestSentTime: 241114303.84899998, - requestStartTime: 241114306.416, loadEndTime: 241114370.361, timings: { send: 1.357000001007691, @@ -292,7 +288,6 @@ const NETWORK_EVENTS = { status: 200, mimeType: 'application/javascript', requestSentTime: 241114305.939, - requestStartTime: 241114310.393, loadEndTime: 241114938.264, timings: { wait: 51.61500000394881, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index a0993d54bbd07..4881fdb6e6b85 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -60,7 +60,7 @@ const StyledButton = styled(EuiButtonEmpty)` } `; -export const getChunks = (text: string) => { +export const getChunks = (text: string = '') => { const END_CHARS = 12; const chars = text.split(''); const splitPoint = chars.length - END_CHARS > 0 ? chars.length - END_CHARS : null; diff --git a/x-pack/plugins/uptime/public/state/reducers/network_events.ts b/x-pack/plugins/uptime/public/state/reducers/network_events.ts index 60f111cb4fafe..56fef5947fb0e 100644 --- a/x-pack/plugins/uptime/public/state/reducers/network_events.ts +++ b/x-pack/plugins/uptime/public/state/reducers/network_events.ts @@ -22,6 +22,7 @@ export interface NetworkEventsState { total: number; loading: boolean; error?: Error; + isWaterfallSupported: boolean; }; }; } @@ -48,11 +49,13 @@ export const networkEventsReducer = handleActions( loading: true, events: [], total: 0, + isWaterfallSupported: true, } : { loading: true, events: [], total: 0, + isWaterfallSupported: true, }, } : { @@ -60,6 +63,7 @@ export const networkEventsReducer = handleActions( loading: true, events: [], total: 0, + isWaterfallSupported: true, }, }, }), @@ -67,7 +71,7 @@ export const networkEventsReducer = handleActions( [String(getNetworkEventsSuccess)]: ( state: NetworkEventsState, { - payload: { events, total, checkGroup, stepIndex }, + payload: { events, total, checkGroup, stepIndex, isWaterfallSupported }, }: Action ) => { return { @@ -80,11 +84,13 @@ export const networkEventsReducer = handleActions( loading: false, events, total, + isWaterfallSupported, } : { loading: false, events, total, + isWaterfallSupported, }, } : { @@ -92,6 +98,7 @@ export const networkEventsReducer = handleActions( loading: false, events, total, + isWaterfallSupported, }, }, }; @@ -111,12 +118,14 @@ export const networkEventsReducer = handleActions( events: [], total: 0, error, + isWaterfallSupported: true, } : { loading: false, events: [], total: 0, error, + isWaterfallSupported: true, }, } : { @@ -125,6 +134,7 @@ export const networkEventsReducer = handleActions( events: [], total: 0, error, + isWaterfallSupported: true, }, }, }), diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts index ba898bab71087..e319eb2383c32 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts @@ -52,96 +52,6 @@ describe('getNetworkEvents', () => { type: 'Image', request_sent_time: 3287.154973, url: 'www.test.com', - request: { - initial_priority: 'Low', - referrer_policy: 'no-referrer-when-downgrade', - url: 'www.test.com', - method: 'GET', - headers: { - referer: 'www.test.com', - user_agent: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36', - }, - mixed_content_type: 'none', - }, - response: { - from_service_worker: false, - security_details: { - protocol: 'TLS 1.2', - key_exchange: 'ECDHE_RSA', - valid_to: 1638230399, - certificate_transparency_compliance: 'unknown', - cipher: 'AES_128_GCM', - issuer: 'DigiCert TLS RSA SHA256 2020 CA1', - subject_name: 'syndication.twitter.com', - valid_from: 1606694400, - signed_certificate_timestamp_list: [], - key_exchange_group: 'P-256', - san_list: [ - 'syndication.twitter.com', - 'syndication.twimg.com', - 'cdn.syndication.twitter.com', - 'cdn.syndication.twimg.com', - 'syndication-o.twitter.com', - 'syndication-o.twimg.com', - ], - certificate_id: 0, - }, - security_state: 'secure', - connection_reused: true, - remote_port: 443, - timing: { - ssl_start: -1, - send_start: 0.214, - ssl_end: -1, - connect_start: -1, - connect_end: -1, - send_end: 0.402, - dns_start: -1, - request_time: 3287.155502, - push_end: 0, - worker_fetch_start: -1, - worker_ready: -1, - worker_start: -1, - proxy_end: -1, - push_start: 0, - worker_respond_with_settled: -1, - proxy_start: -1, - dns_end: -1, - receive_headers_end: 142.215, - }, - connection_id: 852, - remote_i_p_address: '104.244.42.200', - encoded_data_length: 337, - response_time: 1.60794279932414e12, - from_prefetch_cache: false, - mime_type: 'image/gif', - from_disk_cache: false, - url: 'www.test.com', - protocol: 'h2', - headers: { - x_frame_options: 'SAMEORIGIN', - cache_control: 'no-cache, no-store, must-revalidate, pre-check=0, post-check=0', - strict_transport_security: 'max-age=631138519', - x_twitter_response_tags: 'BouncerCompliant', - content_type: 'image/gif;charset=utf-8', - expires: 'Tue, 31 Mar 1981 05:00:00 GMT', - date: 'Mon, 14 Dec 2020 10:46:39 GMT', - x_transaction: '008fff3d00a1e64c', - x_connection_hash: 'cb6fe99b8676f4e4b827cc3e6512c90d', - last_modified: 'Mon, 14 Dec 2020 10:46:39 GMT', - x_content_type_options: 'nosniff', - content_encoding: 'gzip', - x_xss_protection: '0', - server: 'tsa_f', - x_response_time: '108', - pragma: 'no-cache', - content_length: '65', - status: '200 OK', - }, - status_text: '', - status: 200, - }, timings: { proxy: -1, connect: -1, @@ -158,6 +68,99 @@ describe('getNetworkEvents', () => { timestamp: 1607942799183375, }, }, + http: { + response: { + from_service_worker: false, + security_state: 'secure', + connection_reused: true, + remote_port: 443, + timing: { + ssl_start: -1, + send_start: 0.214, + ssl_end: -1, + connect_start: -1, + connect_end: -1, + send_end: 0.402, + dns_start: -1, + request_time: 3287.155502, + push_end: 0, + worker_fetch_start: -1, + worker_ready: -1, + worker_start: -1, + proxy_end: -1, + push_start: 0, + worker_respond_with_settled: -1, + proxy_start: -1, + dns_end: -1, + receive_headers_end: 142.215, + }, + connection_id: 852, + remote_i_p_address: '104.244.42.200', + encoded_data_length: 337, + response_time: 1.60794279932414e12, + from_prefetch_cache: false, + mime_type: 'image/gif', + from_disk_cache: false, + url: 'www.test.com', + protocol: 'h2', + headers: { + x_frame_options: 'SAMEORIGIN', + cache_control: 'no-cache, no-store, must-revalidate, pre-check=0, post-check=0', + strict_transport_security: 'max-age=631138519', + x_twitter_response_tags: 'BouncerCompliant', + content_type: 'image/gif;charset=utf-8', + expires: 'Tue, 31 Mar 1981 05:00:00 GMT', + date: 'Mon, 14 Dec 2020 10:46:39 GMT', + x_transaction: '008fff3d00a1e64c', + x_connection_hash: 'cb6fe99b8676f4e4b827cc3e6512c90d', + last_modified: 'Mon, 14 Dec 2020 10:46:39 GMT', + x_content_type_options: 'nosniff', + content_encoding: 'gzip', + x_xss_protection: '0', + server: 'tsa_f', + x_response_time: '108', + pragma: 'no-cache', + content_length: '65', + status: '200 OK', + }, + status_text: '', + status: 200, + }, + version: 2, + request: { + initial_priority: 'Low', + referrer_policy: 'no-referrer-when-downgrade', + url: 'www.test.com', + method: 'GET', + headers: { + referer: 'www.test.com', + user_agent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36', + }, + mixed_content_type: 'none', + }, + }, + tls: { + server: { + x509: { + subject: { + common_name: 'syndication.twitter.com', + }, + issuer: { + common_name: 'DigiCert TLS RSA SHA256 2020 CA1', + }, + not_before: '2021-02-22T18:35:26.000Z', + not_after: '2021-04-05T22:28:43.000Z', + }, + }, + }, + url: { + port: 443, + path: '', + full: 'www.test.com', + scheme: 'http', + domain: 'www.test.com', + }, }, }, ]; @@ -243,8 +246,8 @@ describe('getNetworkEvents', () => { "certificates": Object { "issuer": "DigiCert TLS RSA SHA256 2020 CA1", "subjectName": "syndication.twitter.com", - "validFrom": 1606694400000, - "validTo": 1638230399000, + "validFrom": "2021-02-22T18:35:26.000Z", + "validTo": "2021-04-05T22:28:43.000Z", }, "ip": "104.244.42.200", "loadEndTime": 3287298.251, @@ -255,7 +258,6 @@ describe('getNetworkEvents', () => { "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36", }, "requestSentTime": 3287154.973, - "requestStartTime": 3287155.502, "responseHeaders": Object { "cache_control": "no-cache, no-store, must-revalidate, pre-check=0, post-check=0", "content_encoding": "gzip", @@ -293,6 +295,7 @@ describe('getNetworkEvents', () => { "url": "www.test.com", }, ], + "isWaterfallSupported": true, "total": 1, } `); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index 246b2001a9381..2741062cc4038 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -20,7 +20,7 @@ export const secondsToMillis = (seconds: number) => export const getNetworkEvents: UMElasticsearchQueryFn< GetNetworkEventsParams, - { events: NetworkEvent[]; total: number } + { events: NetworkEvent[]; total: number; isWaterfallSupported: boolean } > = async ({ uptimeEsClient, checkGroup, stepIndex }) => { const params = { track_total_hits: true, @@ -40,46 +40,42 @@ export const getNetworkEvents: UMElasticsearchQueryFn< }; const { body: result } = await uptimeEsClient.search({ body: params }); + let isWaterfallSupported = false; + const events = result.hits.hits.map((event: any) => { + if (event._source.http && event._source.url) { + isWaterfallSupported = true; + } + const requestSentTime = secondsToMillis(event._source.synthetics.payload.request_sent_time); + const loadEndTime = secondsToMillis(event._source.synthetics.payload.load_end_time); + const securityDetails = event._source.tls?.server?.x509; + + return { + timestamp: event._source['@timestamp'], + method: event._source.http?.request?.method, + url: event._source.url?.full, + status: event._source.http?.response?.status, + mimeType: event._source.http?.response?.mime_type, + requestSentTime, + loadEndTime, + timings: event._source.synthetics.payload.timings, + bytesDownloadedCompressed: event._source.http?.response?.encoded_data_length, + certificates: securityDetails + ? { + issuer: securityDetails.issuer?.common_name, + subjectName: securityDetails.subject.common_name, + validFrom: securityDetails.not_before, + validTo: securityDetails.not_after, + } + : undefined, + requestHeaders: event._source.http?.request?.headers, + responseHeaders: event._source.http?.response?.headers, + ip: event._source.http?.response?.remote_i_p_address, + }; + }); return { total: result.hits.total.value, - events: result.hits.hits.map((event: any) => { - const requestSentTime = secondsToMillis(event._source.synthetics.payload.request_sent_time); - const loadEndTime = secondsToMillis(event._source.synthetics.payload.load_end_time); - const requestStartTime = - event._source.synthetics.payload.response && - event._source.synthetics.payload.response.timing - ? secondsToMillis(event._source.synthetics.payload.response.timing.request_time) - : undefined; - const securityDetails = event._source.synthetics.payload.response?.security_details; - - return { - timestamp: event._source['@timestamp'], - method: event._source.synthetics.payload?.method, - url: event._source.synthetics.payload?.url, - status: event._source.synthetics.payload?.status, - mimeType: event._source.synthetics.payload?.response?.mime_type, - requestSentTime, - requestStartTime, - loadEndTime, - timings: event._source.synthetics.payload.timings, - bytesDownloadedCompressed: event._source.synthetics.payload.response?.encoded_data_length, - certificates: securityDetails - ? { - issuer: securityDetails.issuer, - subjectName: securityDetails.subject_name, - validFrom: securityDetails.valid_from - ? secondsToMillis(securityDetails.valid_from) - : undefined, - validTo: securityDetails.valid_to - ? secondsToMillis(securityDetails.valid_to) - : undefined, - } - : undefined, - requestHeaders: event._source.synthetics.payload.request?.headers, - responseHeaders: event._source.synthetics.payload.response?.headers, - ip: event._source.synthetics.payload.response?.remote_i_p_address, - }; - }), + events, + isWaterfallSupported, }; }; diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts index 1761c44813430..deb91f6b9b1ef 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @@ -97,6 +97,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickCreateDashboardPrompt(); await ml.dashboardEmbeddables.assertDashboardIsEmpty(); await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.ensureAddPanelIsShowing(); await dashboardAddPanel.clickAddNewEmbeddableLink('ml_anomaly_charts'); await ml.dashboardJobSelectionTable.assertJobSelectionTableExists(); await a11y.testAppSnapshot(); diff --git a/x-pack/test/fleet_api_integration/apis/agents_setup.ts b/x-pack/test/fleet_api_integration/apis/agents_setup.ts index 91d6ca0119d1d..700a06750d2f4 100644 --- a/x-pack/test/fleet_api_integration/apis/agents_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/agents_setup.ts @@ -101,7 +101,7 @@ export default function (providerContext: FtrProviderContext) { ); }); - it('should create or update the fleet_enroll user if called multiple times with forceRecreate flag', async () => { + it.skip('should create or update the fleet_enroll user if called multiple times with forceRecreate flag', async () => { await supertest.post(`/api/fleet/agents/setup`).set('kbn-xsrf', 'xxxx').expect(200); const { body: userResponseFirstTime } = await es.security.getUser({ diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 2569d9aef4b5b..d9946bb174f5d 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -115,6 +115,28 @@ export default function (providerContext: FtrProviderContext) { expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); }); + it('should create an ES ApiKey with metadata', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + }) + .expect(200); + + const { body: apiKeyRes } = await es.security.getApiKey({ + id: apiResponse.item.api_key_id, + }); + + // @ts-expect-error Metadata not yet in the client type + expect(apiKeyRes.api_keys[0].metadata).eql({ + policy_id: 'policy1', + managed_by: 'fleet', + managed: true, + type: 'enroll', + }); + }); + it('should create an ES ApiKey with limited privileges', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) @@ -162,33 +184,6 @@ export default function (providerContext: FtrProviderContext) { }, }); }); - - describe('It should handle error when the Fleet user is invalid', () => { - before(async () => {}); - after(async () => { - await getService('supertest') - .post(`/api/fleet/agents/setup`) - .set('kbn-xsrf', 'xxx') - .send({ forceRecreate: true }); - }); - - it('should not allow to create an enrollment api key if the Fleet admin user is invalid', async () => { - await es.security.changePassword({ - username: 'fleet_enroll', - body: { - password: Buffer.from((Math.random() * 10000000).toString()).toString('base64'), - }, - }); - const res = await supertest - .post(`/api/fleet/enrollment-api-keys`) - .set('kbn-xsrf', 'xxx') - .send({ - policy_id: 'policy1', - }) - .expect(400); - expect(res.body.message).match(/Fleet Admin user is invalid/); - }); - }); }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index 3764bdbc20d03..e2e1cc2f584bb 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -6,19 +6,18 @@ */ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../../helpers'; +import { skipIfNoDockerRegistry } from '../../helpers'; -export default function ({ getService }: FtrProviderContext) { - const log = getService('log'); +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const supertest = getService('supertest'); - const dockerServers = getService('dockerServers'); - const server = dockerServers.get('registry'); // use function () {} and not () => {} here // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions describe('Package Policy - create', async function () { + skipIfNoDockerRegistry(providerContext); let agentPolicyId: string; before(async () => { await getService('esArchiver').load('empty_kibana'); @@ -47,230 +46,229 @@ export default function ({ getService }: FtrProviderContext) { .send({ agentPolicyId }); }); - it('should fail for managed agent policies', async function () { - if (server.enabled) { - // get a managed policy - const { - body: { item: managedPolicy }, - } = await supertest - .post(`/api/fleet/agent_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: `Managed policy from ${Date.now()}`, - namespace: 'default', - is_managed: true, - }); + it('can only add to managed agent policies using the force parameter', async function () { + // get a managed policy + const { + body: { item: managedPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `Managed policy from ${Date.now()}`, + namespace: 'default', + is_managed: true, + }); - // try to add an integration to the managed policy - const { body } = await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'filetest-1', - description: '', - namespace: 'default', - policy_id: managedPolicy.id, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'filetest', - title: 'For File Tests', - version: '0.1.0', - }, - }) - .expect(400); + // try to add an integration to the managed policy + const { body: responseWithoutForce } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + policy_id: managedPolicy.id, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); - expect(body.statusCode).to.be(400); - expect(body.message).to.contain('Cannot add integrations to managed policy'); + expect(responseWithoutForce.statusCode).to.be(400); + expect(responseWithoutForce.message).to.contain('Cannot add integrations to managed policy'); - // delete policy we just made - await supertest.post(`/api/fleet/agent_policies/delete`).set('kbn-xsrf', 'xxxx').send({ - agentPolicyId: managedPolicy.id, - }); - } else { - warnAndSkipTest(this, log); - } + // try same request with `force: true` + const { body: responseWithForce } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + force: true, + name: 'filetest-1', + description: '', + namespace: 'default', + policy_id: managedPolicy.id, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(200); + + expect(responseWithForce.item.name).to.eql('filetest-1'); + + // delete policy we just made + await supertest.post(`/api/fleet/agent_policies/delete`).set('kbn-xsrf', 'xxxx').send({ + agentPolicyId: managedPolicy.id, + }); }); it('should work with valid values', async function () { - if (server.enabled) { - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'filetest-1', - description: '', - namespace: 'default', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'filetest', - title: 'For File Tests', - version: '0.1.0', - }, - }) - .expect(200); - } else { - warnAndSkipTest(this, log); - } + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(200); }); it('should return a 400 with an empty namespace', async function () { - if (server.enabled) { - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'filetest-1', - description: '', - namespace: '', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'filetest', - title: 'For File Tests', - version: '0.1.0', - }, - }) - .expect(400); - } else { - warnAndSkipTest(this, log); - } + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: '', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); }); it('should return a 400 with an invalid namespace', async function () { - if (server.enabled) { - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'filetest-1', - description: '', - namespace: 'InvalidNamespace', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'filetest', - title: 'For File Tests', - version: '0.1.0', - }, - }) - .expect(400); - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'filetest-1', - description: '', - namespace: - 'testlength😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'filetest', - title: 'For File Tests', - version: '0.1.0', - }, - }) - .expect(400); - } else { - warnAndSkipTest(this, log); - } + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'InvalidNamespace', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: + 'testlength😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); }); it('should not allow multiple limited packages on the same agent policy', async function () { - if (server.enabled) { - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'endpoint-1', - description: '', - namespace: 'default', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'endpoint', - title: 'Endpoint', - version: '0.13.0', - }, - }) - .expect(200); - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'endpoint-2', - description: '', - namespace: 'default', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'endpoint', - title: 'Endpoint', - version: '0.13.0', - }, - }) - .expect(500); - } else { - warnAndSkipTest(this, log); - } + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'endpoint', + title: 'Endpoint', + version: '0.13.0', + }, + }) + .expect(200); + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-2', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'endpoint', + title: 'Endpoint', + version: '0.13.0', + }, + }) + .expect(400); }); - it('should return a 500 if there is another package policy with the same name', async function () { - if (server.enabled) { - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'same-name-test-1', - description: '', - namespace: 'default', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'filetest', - title: 'For File Tests', - version: '0.1.0', - }, - }) - .expect(200); - await supertest - .post(`/api/fleet/package_policies`) - .set('kbn-xsrf', 'xxxx') - .send({ - name: 'same-name-test-1', - description: '', - namespace: 'default', - policy_id: agentPolicyId, - enabled: true, - output_id: '', - inputs: [], - package: { - name: 'filetest', - title: 'For File Tests', - version: '0.1.0', - }, - }) - .expect(500); - } else { - warnAndSkipTest(this, log); - } + it('should return a 400 if there is another package policy with the same name', async function () { + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'same-name-test-1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(200); + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'same-name-test-1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts index 85e6f5ab92b74..15aba758c85d0 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts @@ -80,7 +80,7 @@ export default function (providerContext: FtrProviderContext) { await supertest .post(`/api/fleet/package_policies/delete`) .set('kbn-xsrf', 'xxxx') - .send({ packagePolicyIds: [packagePolicy.id] }); + .send({ force: true, packagePolicyIds: [packagePolicy.id] }); }); after(async () => { await getService('esArchiver').unload('empty_kibana'); @@ -112,6 +112,18 @@ export default function (providerContext: FtrProviderContext) { expect(results[0].success).to.be(false); expect(results[0].body.message).to.contain('Cannot remove integrations of managed policy'); + // same, but with force + const { body: resultsWithForce } = await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true, packagePolicyIds: [packagePolicy.id] }) + .expect(200); + + // delete always succeeds (returns 200) with Array<{success: boolean}> + expect(Array.isArray(resultsWithForce)); + expect(resultsWithForce.length).to.be(1); + expect(resultsWithForce[0].success).to.be(true); + // revert existing policy to unmanaged await supertest .put(`/api/fleet/agent_policies/${agentPolicy.id}`) diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts index 3e652d47ac425..6e6a475cd4824 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -115,15 +114,15 @@ export default function (providerContext: FtrProviderContext) { await getService('esArchiver').unload('empty_kibana'); }); - it('should fail on managed agent policies', async function () { - const { body } = await supertest + it('should work with valid values on "regular" policies', async function () { + await supertest .put(`/api/fleet/package_policies/${packagePolicyId}`) .set('kbn-xsrf', 'xxxx') .send({ name: 'filetest-1', description: '', namespace: 'updated_namespace', - policy_id: managedAgentPolicyId, + policy_id: agentPolicyId, enabled: true, output_id: '', inputs: [], @@ -132,13 +131,10 @@ export default function (providerContext: FtrProviderContext) { title: 'For File Tests', version: '0.1.0', }, - }) - .expect(400); - - expect(body.message).to.contain('Cannot update integrations of managed policy'); + }); }); - it('should work with valid values', async function () { + it('should work with valid values on hosted policies', async function () { await supertest .put(`/api/fleet/package_policies/${packagePolicyId}`) .set('kbn-xsrf', 'xxxx') @@ -146,7 +142,7 @@ export default function (providerContext: FtrProviderContext) { name: 'filetest-1', description: '', namespace: 'updated_namespace', - policy_id: agentPolicyId, + policy_id: managedAgentPolicyId, enabled: true, output_id: '', inputs: [], diff --git a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts index 1619765946916..f7bfd7f7a4c62 100644 --- a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts @@ -88,6 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickCreateDashboardPrompt(); await ml.dashboardEmbeddables.assertDashboardIsEmpty(); await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.ensureAddPanelIsShowing(); await dashboardAddPanel.clickAddNewEmbeddableLink('ml_anomaly_charts'); await ml.dashboardJobSelectionTable.assertJobSelectionTableExists(); }); diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index d0c7c86d6d5c3..891805acb3256 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -83,6 +83,7 @@ export default function ({ getService, getPageObjects }) { ); await PageObjects.visualBuilder.clickPanelOptions('metric'); await PageObjects.visualBuilder.setIndexPatternValue(rollupTargetIndexName, false); + await PageObjects.visualBuilder.selectIndexPatternTimeField('@timestamp'); await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value'); await PageObjects.visualBuilder.setIntervalValue('1d'); await PageObjects.visualBuilder.setDropLastBucket(false); diff --git a/x-pack/test/stack_functional_integration/apps/ccs/ccs.js b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js similarity index 77% rename from x-pack/test/stack_functional_integration/apps/ccs/ccs.js rename to x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js index c335680fbc6f9..588ff9a6e9f92 100644 --- a/x-pack/test/stack_functional_integration/apps/ccs/ccs.js +++ b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; export default ({ getService, getPageObjects }) => { - describe('Cross cluster search test', async () => { + describe('Cross cluster search test in discover', async () => { const PageObjects = getPageObjects([ 'common', 'settings', @@ -22,10 +22,12 @@ export default ({ getService, getPageObjects }) => { const browser = getService('browser'); const appsMenu = getService('appsMenu'); const kibanaServer = getService('kibanaServer'); + const queryBar = getService('queryBar'); + const filterBar = getService('filterBar'); before(async () => { await browser.setWindowSize(1200, 800); - // pincking relative time in timepicker isn't working. This is also faster. + // picking relative time in timepicker isn't working. This is also faster. // It's the default set, plus new "makelogs" +/- 3 days from now await kibanaServer.uiSettings.replace({ 'timepicker:quickRanges': `[ @@ -172,5 +174,34 @@ export default ({ getService, getPageObjects }) => { expect(hitCount).to.be('28,010'); }); }); + + it('should reload the saved search with persisted query to show the initial hit count', async function () { + await PageObjects.discover.selectIndexPattern('data:makelogs工程-*,local:makelogs工程-*'); + // apply query some changes + await queryBar.setQuery('success'); + await queryBar.submitQuery(); + await retry.try(async () => { + const hitCountNumber = await PageObjects.discover.getHitCount(); + const hitCount = parseInt(hitCountNumber.replace(/\,/g, '')); + log.debug('### hit count = ' + hitCount); + expect(hitCount).to.be.greaterThan(25000); + expect(hitCount).to.be.lessThan(28000); + }); + }); + + it('should add a phrases filter', async function () { + await PageObjects.discover.selectIndexPattern('data:makelogs工程-*,local:makelogs工程-*'); + const hitCountNumber = await PageObjects.discover.getHitCount(); + const originalHitCount = parseInt(hitCountNumber.replace(/\,/g, '')); + await filterBar.addFilter('extension.keyword', 'is', 'jpg'); + expect(await filterBar.hasFilter('extension.keyword', 'jpg')).to.be(true); + await retry.try(async () => { + const hitCountNumber = await PageObjects.discover.getHitCount(); + const hitCount = parseInt(hitCountNumber.replace(/\,/g, '')); + log.debug('### hit count = ' + hitCount); + expect(hitCount).to.be.greaterThan(15000); + expect(hitCount).to.be.lessThan(originalHitCount); + }); + }); }); }; diff --git a/x-pack/test/stack_functional_integration/apps/ccs/index.js b/x-pack/test/stack_functional_integration/apps/ccs/index.js index dd87414c2b9f0..ac82ca0dfda65 100644 --- a/x-pack/test/stack_functional_integration/apps/ccs/index.js +++ b/x-pack/test/stack_functional_integration/apps/ccs/index.js @@ -7,6 +7,6 @@ export default function ({ loadTestFile }) { describe('ccs test', function () { - loadTestFile(require.resolve('./ccs')); + loadTestFile(require.resolve('./ccs_discover')); }); } diff --git a/yarn.lock b/yarn.lock index e1118cf0b74fd..991ba9f59da20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10950,11 +10950,6 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" -custom-event-polyfill@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz#99807839be62edb446b645832e0d80ead6fa1888" - integrity sha1-mYB4Ob5i7bRGtkWDLg2A6tb6GIg= - cwise-compiler@^1.0.0, cwise-compiler@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5"