diff --git a/.eslintrc.js b/.eslintrc.js index 8a6ea7957927a..211aed1da7279 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1114,6 +1114,118 @@ module.exports = { 'prefer-destructuring': 'error', }, }, + /** + * Metrics entities overrides + */ + { + // front end and common typescript and javascript files only + files: [ + 'x-pack/plugins/metrics_entities/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/metrics_entities/common/**/*.{js,mjs,ts,tsx}', + ], + rules: { + 'import/no-nodejs-modules': 'error', + 'no-restricted-imports': [ + 'error', + { + // prevents UI code from importing server side code and then webpack including it when doing builds + patterns: ['**/server/*'], + }, + ], + }, + }, + { + // typescript and javascript for front and back end + files: ['x-pack/plugins/metrics_entities/**/*.{js,mjs,ts,tsx}'], + plugins: ['eslint-plugin-node'], + env: { + jest: true, + }, + rules: { + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'no-array-constructor': 'error', + complexity: 'error', + 'consistent-return': 'error', + 'func-style': ['error', 'expression'], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + }, + ], + 'sort-imports': [ + 'error', + { + ignoreDeclarationSort: true, + }, + ], + 'node/no-deprecated-api': 'error', + 'no-bitwise': 'error', + 'no-continue': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty-character-class': 'error', + 'no-empty-pattern': 'error', + 'no-ex-assign': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-label': 'error', + 'no-func-assign': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-invalid-regexp': 'error', + 'no-inner-declarations': 'error', + 'no-lone-blocks': 'error', + 'no-multi-assign': 'error', + 'no-misleading-character-class': 'error', + 'no-new-symbol': 'error', + 'no-obj-calls': 'error', + 'no-param-reassign': ['error', { props: true }], + 'no-process-exit': 'error', + 'no-prototype-builtins': 'error', + 'no-return-await': 'error', + 'no-self-compare': 'error', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-this-before-super': 'error', + // rely on typescript + 'no-undef': 'off', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-concat': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-escape': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-void': 'error', + 'one-var-declaration-per-line': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'require-atomic-updates': 'error', + 'symbol-description': 'error', + 'vars-on-top': 'error', + '@typescript-eslint/explicit-member-accessibility': 'error', + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/unified-signatures': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-unused-vars': 'error', + 'no-template-curly-in-string': 'error', + 'sort-keys': 'error', + 'prefer-destructuring': 'error', + }, + }, /** * Alerting Services overrides */ diff --git a/Jenkinsfile b/Jenkinsfile index 8ab3fecb07a1b..4c8f126b4883b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,7 +6,7 @@ kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 210, checkPrChanges: true, setCommitStatus: true) { slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { githubPr.withDefaultPrComments { - ciStats.trackBuild { + ciStats.trackBuild(requireSuccess: githubPr.isPr()) { catchError { retryable.enable() kibanaPipeline.allCiTasks() diff --git a/api_docs/metrics_entities.json b/api_docs/metrics_entities.json new file mode 100644 index 0000000000000..3b05ca066b0e2 --- /dev/null +++ b/api_docs/metrics_entities.json @@ -0,0 +1,151 @@ +{ + "id": "metricsEntities", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "id": "def-server.MetricsEntitiesPluginSetup", + "type": "Interface", + "label": "MetricsEntitiesPluginSetup", + "description": [], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-server.MetricsEntitiesPluginSetup.getMetricsEntitiesClient", + "type": "Function", + "label": "getMetricsEntitiesClient", + "description": [], + "source": { + "path": "x-pack/plugins/metrics_entities/server/types.ts", + "lineNumber": 15 + }, + "signature": [ + "GetMetricsEntitiesClientType" + ] + } + ], + "source": { + "path": "x-pack/plugins/metrics_entities/server/types.ts", + "lineNumber": 14 + }, + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "id": "def-server.MetricsEntitiesPluginStart", + "type": "Type", + "label": "MetricsEntitiesPluginStart", + "tags": [], + "description": [], + "source": { + "path": "x-pack/plugins/metrics_entities/server/types.ts", + "lineNumber": 18 + }, + "signature": [ + "void" + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "tags": [], + "id": "def-common.ELASTIC_NAME", + "type": "string", + "label": "ELASTIC_NAME", + "description": [ + "\nGlobal prefix for all the transform jobs" + ], + "source": { + "path": "x-pack/plugins/metrics_entities/common/constants.ts", + "lineNumber": 21 + }, + "signature": [ + "\"estc\"" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.METRICS_ENTITIES_TRANSFORMS", + "type": "string", + "label": "METRICS_ENTITIES_TRANSFORMS", + "description": [ + "\nTransforms route" + ], + "source": { + "path": "x-pack/plugins/metrics_entities/common/constants.ts", + "lineNumber": 16 + }, + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.METRICS_ENTITIES_URL", + "type": "string", + "label": "METRICS_ENTITIES_URL", + "description": [ + "\nBase route" + ], + "source": { + "path": "x-pack/plugins/metrics_entities/common/constants.ts", + "lineNumber": 11 + }, + "signature": [ + "\"/api/metrics_entities\"" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.PLUGIN_ID", + "type": "string", + "label": "PLUGIN_ID", + "description": [], + "source": { + "path": "x-pack/plugins/metrics_entities/common/index.ts", + "lineNumber": 8 + }, + "signature": [ + "\"metricsEntities\"" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.PLUGIN_NAME", + "type": "string", + "label": "PLUGIN_NAME", + "description": [], + "source": { + "path": "x-pack/plugins/metrics_entities/common/index.ts", + "lineNumber": 9 + }, + "signature": [ + "\"metrics_entities\"" + ], + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/metrics_entities.mdx b/api_docs/metrics_entities.mdx new file mode 100644 index 0000000000000..19a27636511c3 --- /dev/null +++ b/api_docs/metrics_entities.mdx @@ -0,0 +1,26 @@ +--- +id: kibMetricsEntitiesPluginApi +slug: /kibana-dev-docs/metricsEntitiesPluginApi +title: metricsEntities +image: https://source.unsplash.com/400x175/?github +summary: API docs for the metricsEntities plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'metricsEntities'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- + +import metricsEntitiesObj from './metrics_entities.json'; + +## Server + +### Setup + + +### Start + + +## Common + +### Consts, variables and types + + diff --git a/docs/apm/images/error-rate.png b/docs/apm/images/error-rate.png index 7e5e32c50f13e..b43a0cb5d1a78 100644 Binary files a/docs/apm/images/error-rate.png and b/docs/apm/images/error-rate.png differ diff --git a/docs/apm/images/latency.png b/docs/apm/images/latency.png index 4c970d8c582e6..1c220c1a4bfdd 100644 Binary files a/docs/apm/images/latency.png and b/docs/apm/images/latency.png differ diff --git a/docs/apm/images/metadata-icons.png b/docs/apm/images/metadata-icons.png index dcdac41a7d01a..402c0ed07c70d 100644 Binary files a/docs/apm/images/metadata-icons.png and b/docs/apm/images/metadata-icons.png differ diff --git a/docs/apm/images/spans-dependencies.png b/docs/apm/images/spans-dependencies.png index a827083b5ddcd..d6e26a5061a6e 100644 Binary files a/docs/apm/images/spans-dependencies.png and b/docs/apm/images/spans-dependencies.png differ diff --git a/docs/apm/images/time-series-comparison.png b/docs/apm/images/time-series-comparison.png new file mode 100644 index 0000000000000..6d3cdf4a1634f Binary files /dev/null and b/docs/apm/images/time-series-comparison.png differ diff --git a/docs/apm/images/traffic-transactions.png b/docs/apm/images/traffic-transactions.png index ef429740ceee3..05e66dfaa4ece 100644 Binary files a/docs/apm/images/traffic-transactions.png and b/docs/apm/images/traffic-transactions.png differ diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 693046d652943..f1096a4e43bbc 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -4,7 +4,41 @@ Selecting a <> brings you to the *Service overview*. The *Service overview* contains a wide variety of charts and tables that provide -visibility into how a service performs across your infrastructure. +high-level visibility into how a service is performing across your infrastructure: + +* Service details like service version, runtime version, framework, and agent name and version +* Container and orchestration information +* Cloud provider, machine type, and availability zone +* Latency, throughput, and errors over time +* Service dependencies + +[discrete] +[[service-time-comparison]] +=== Time series comparison + +Comparing how a service performs relative to a previous time frame can offer additional insight into +the health of your services. For example, has latency been slowly increasing over time, or did the service +experience a sudden spike--enabling a time series comparison can provide the answer. + +[role="screenshot"] +image::apm/images/time-series-comparison.png[Time series comparison] + +Select the *Comparison* box to enable or disable time series comparison. +The time comparison options are based on the selected time filter range: + +[options="header"] +|==== +|Time filter | Time comparison options + +|≤ 24 hours +|One day or one week + +|> 24 hours and ≤ 7 days +|One week + +|> 7 days +|An identical amount of time immediately before the selected time range +|==== [discrete] [[service-latency]] @@ -111,3 +145,7 @@ image::apm/images/metadata-icons.png[Service metadata] * Availability zones * Machine types * Project ID + +*Alerts* + +* Recently fired alerts diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc index 983ab671cbd53..74e5bd4d4fb2f 100644 --- a/docs/concepts/index.asciidoc +++ b/docs/concepts/index.asciidoc @@ -55,6 +55,7 @@ dates, geopoints, and numbers. [float] +[[kibana-concepts-searching-your-data]] === Searching your data {kib} provides you several ways to build search queries, diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 31a153cdb3490..969226df53cb7 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -70,7 +70,10 @@ yarn kbn watch-bazel - @kbn/babel-preset - @kbn/config-schema - @kbn/dev-utils +- @kbn/eslint-import-resolver-kibana +- @kbn/eslint-plugin-eslint - @kbn/expect +- @kbn/legacy-logging - @kbn/logging - @kbn/std - @kbn/tinymath diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index ad58cd040ff35..6f94ce6cec3bd 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -474,6 +474,12 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. +|{kib-repo}blob/{branch}/x-pack/plugins/metrics_entities/README.md[metricsEntities] +|This is the metrics and entities plugin where you add can add transforms for your project +and group those transforms into modules. You can also re-use existing transforms in your +modules as well. + + |{kib-repo}blob/{branch}/x-pack/plugins/ml/readme.md[ml] |This plugin provides access to the machine learning features provided by Elastic. 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 3a383ee72b86a..1830e8f140e60 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 @@ -134,6 +134,9 @@ readonly links: { readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; + byteSizeUnits: string; + createAutoFollowPattern: string; + createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; @@ -153,6 +156,7 @@ readonly links: { putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; + timeUnits: 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 c5bf4babd9da9..4242159ff3c20 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 canvas: {
readonly guide: string;
};
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 overview: string;
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 upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: 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<{
bulkIndexAlias: string;
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;
putIndexTemplateV1: 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>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
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 overview: string;
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 upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: 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<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
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;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: 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/maps/images/locked_tooltip.png b/docs/maps/images/locked_tooltip.png index 2ffb5f6935259..3c8ebce033d65 100644 Binary files a/docs/maps/images/locked_tooltip.png and b/docs/maps/images/locked_tooltip.png differ diff --git a/docs/maps/images/multifeature_tooltip.png b/docs/maps/images/multifeature_tooltip.png index e4d5565d91145..9c72f31b3b40b 100644 Binary files a/docs/maps/images/multifeature_tooltip.png and b/docs/maps/images/multifeature_tooltip.png differ diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index 8f55697249fb2..e4150fc280096 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -59,7 +59,7 @@ Customize each layer to highlight meaningful dimensions in your data. For exampl [float] === Focus on only the data that’s important to you -Search across your Elasticsearch layers to focus in on just the data you want. Combine free text search with field-based search using the <>. Set the time filter to restrict layers by time. Draw a polygon on the map or use the shape from features to create spatial filters. Filter individual layers to compares facets. +Search across the layers in your map to focus in on just the data you want. Combine free text search with field-based search using the <>. Set the time filter to restrict layers by time. Draw a polygon on the map or use the shape from features to create spatial filters. Filter individual layers to compares facets. -- diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 70bfe50e6e34a..031c7be077f52 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -2,10 +2,12 @@ [[maps-search]] == Search geographic data -Use the filters, query bar, and time filter to focus in on just the data you want. -Only layers requesting data from {es} are narrowed when you submit a search request. -Layers narrowed by the filters and query bar contain the filter icon image:maps/images/filter_icon.png[] next to the layer name in the legend. -Only layers requesting data from {es} using an <> with a configured time field are narrowed by the time filter. +Search across the layers in your map to focus in on just the data you want. + +Layers that request data from {es} are narrowed when you submit a <>. +Layers narrowed by semi-structured search and filters contain the filter icon image:maps/images/filter_icon.png[] next to the layer name in the legend. + +Layers that request data from {es} using an <> with a configured time field are narrowed by the <>. Layers narrowed by the time filter contain the clock icon image:maps/images/clock_icon.png[] next to the layer name in the legend. You can create a layer that requests data from {es} from the following: @@ -20,6 +22,8 @@ You can create a layer that requests data from {es} from the following: ** <> +** Top hits per entity + ** Tracks * <> @@ -47,14 +51,14 @@ A spatial filter narrows search results to documents that either intersect with, You can create spatial filters in two ways: -* Click the tool icon image:maps/images/tools_icon.png[], and then draw a polygon or bounding box on the map to define the spatial filter. +* Click the tool icon image:maps/images/tools_icon.png[], and then draw a shape, bounding box, or distance on the map to define the spatial filter. * Click *Filter by geometry* in a <>, and then use the feature's geometry for the spatial filter. Spatial filters have the following properties: * *Geometry label* enables you to provide a meaningful name for your spatial filter. * *Spatial field* specifies the geo_point or geo_shape field used to determine if a document matches the spatial relation with the specified geometry. -* *Spatial relation* determines the {ref}/query-dsl-geo-shape-query.html#_spatial_relations[spatial relation operator] to use at search time. Only available when *Spatial field* is set to geo_shape. +* *Spatial relation* determines the {ref}/query-dsl-geo-shape-query.html#_spatial_relations[spatial relation operator] to use at search time. * *Action* specifies whether to apply the filter to the current view or to a drilldown action. Only available when the map is a panel in a {kibana-ref}/dashboard.html[dashboard] with {kibana-ref}/drilldowns.html[drilldowns]. [float] diff --git a/docs/maps/vector-tooltips.asciidoc b/docs/maps/vector-tooltips.asciidoc index a8eb6c20bae77..b0498c9088e4e 100644 --- a/docs/maps/vector-tooltips.asciidoc +++ b/docs/maps/vector-tooltips.asciidoc @@ -6,8 +6,8 @@ These tooltips give users an in-depth insight into what's going on in the map. If more than one feature exists at a location, the tooltip displays the attributes for the top feature, and notes the number of features at that location. -The following image shows a tooltip with three features at the current location. -The tooltip displays attributes for the top feature, the green circle. +The following image has a tooltip with three features at the current location: a green circle from the *Total Sales Revenue* layer, a blue New York State polygon from *United States* layer, and a red United States Country polygon from the *World Countries* layer. +The tooltip displays attributes for the top feature, the green circle, from the *Total Sales Revenue* layer. [role="screenshot"] image::maps/images/multifeature_tooltip.png[] diff --git a/package.json b/package.json index 6be19669d25e1..471ba0d219ff7 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "@kbn/i18n": "link:packages/kbn-i18n", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", - "@kbn/legacy-logging": "link:packages/kbn-legacy-logging", + "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", @@ -444,8 +444,8 @@ "@kbn/docs-utils": "link:packages/kbn-docs-utils", "@kbn/es": "link:packages/kbn-es", "@kbn/es-archiver": "link:packages/kbn-es-archiver", - "@kbn/eslint-import-resolver-kibana": "link:packages/kbn-eslint-import-resolver-kibana", - "@kbn/eslint-plugin-eslint": "link:packages/kbn-eslint-plugin-eslint", + "@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana/npm_module", + "@kbn/eslint-plugin-eslint": "link:bazel-bin/packages/kbn-eslint-plugin-eslint/npm_module", "@kbn/expect": "link:bazel-bin/packages/kbn-expect/npm_module", "@kbn/optimizer": "link:packages/kbn-optimizer", "@kbn/plugin-generator": "link:packages/kbn-plugin-generator", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 2aec108f97047..39285fb9ea66a 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -12,7 +12,10 @@ filegroup( "//packages/kbn-babel-preset:build", "//packages/kbn-config-schema:build", "//packages/kbn-dev-utils:build", + "//packages/kbn-eslint-import-resolver-kibana:build", + "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", + "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-std:build", "//packages/kbn-tinymath:build", diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts index 2fd53dd83a1bd..f4017df600a48 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts @@ -75,6 +75,7 @@ it('produces the right watch and ignore list', () => { /x-pack/plugins/lists/server/scripts, /x-pack/plugins/security_solution/scripts, /x-pack/plugins/security_solution/server/lib/detection_engine/scripts, + /x-pack/plugins/metrics_entities/server/scripts, ] `); }); diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts index 4a9dae5c6fee2..b0773fd567635 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts @@ -66,6 +66,7 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) { fromRoot('x-pack/plugins/lists/server/scripts'), fromRoot('x-pack/plugins/security_solution/scripts'), fromRoot('x-pack/plugins/security_solution/server/lib/detection_engine/scripts'), + fromRoot('x-pack/plugins/metrics_entities/server/scripts'), ]; return { diff --git a/packages/kbn-eslint-import-resolver-kibana/BUILD.bazel b/packages/kbn-eslint-import-resolver-kibana/BUILD.bazel new file mode 100644 index 0000000000000..a4d96f76053e1 --- /dev/null +++ b/packages/kbn-eslint-import-resolver-kibana/BUILD.bazel @@ -0,0 +1,54 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-eslint-import-resolver-kibana" +PKG_REQUIRE_NAME = "@kbn/eslint-import-resolver-kibana" + +SOURCE_FILES = glob([ + "lib/**/*.js", + "import_resolver_kibana.js", +]) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +DEPS = [ + "@npm//debug", + "@npm//eslint-import-resolver-node", + "@npm//eslint-import-resolver-webpack", + "@npm//eslint-plugin-import", + "@npm//lru-cache", +] + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [ + ":srcs", + ], + deps = DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-eslint-plugin-eslint/BUILD.bazel b/packages/kbn-eslint-plugin-eslint/BUILD.bazel new file mode 100644 index 0000000000000..0ea6a4a80be06 --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/BUILD.bazel @@ -0,0 +1,61 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-eslint-plugin-eslint" +PKG_REQUIRE_NAME = "@kbn/eslint-plugin-eslint" + +SOURCE_FILES = glob( + [ + "rules/**/*.js", + "index.js", + "lib.js", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +DEPS = [ + "@npm//babel-eslint", + "@npm//dedent", + "@npm//eslint", + "@npm//eslint-module-utils", + "@npm//micromatch", +] + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [ + ":srcs", + ], + deps = DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel new file mode 100644 index 0000000000000..1cb7ae8d83fdf --- /dev/null +++ b/packages/kbn-legacy-logging/BUILD.bazel @@ -0,0 +1,92 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-legacy-logging" +PKG_REQUIRE_NAME = "@kbn/legacy-logging" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config-schema", + "@npm//@elastic/numeral", + "@npm//@hapi/hapi", + "@npm//chokidar", + "@npm//lodash", + "@npm//moment-timezone", + "@npm//query-string", + "@npm//rxjs", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/hapi__hapi", + "@npm//@types/hapi__podium", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/moment-timezone", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json index 8c26535b9b48f..77fcbb9904919 100644 --- a/packages/kbn-legacy-logging/package.json +++ b/packages/kbn-legacy-logging/package.json @@ -4,10 +4,5 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", - "types": "./target/index.d.ts", - "scripts": { - "build": "tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } + "types": "./target/index.d.ts" } diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json index 5f8d38ec90bcd..e3bcedd3de014 100644 --- a/packages/kbn-legacy-logging/tsconfig.json +++ b/packages/kbn-legacy-logging/tsconfig.json @@ -1,19 +1,15 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "stripInternal": false, "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-legacy-logging/src", - "types": [ - "jest", - "node" - ] + "types": ["jest", "node"] }, - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 0bb4594244a75..2e14fb966a2e7 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -67,12 +67,12 @@ pageLoadAssetSize: savedObjectsTagging: 59482 savedObjectsTaggingOss: 20590 searchprofiler: 67080 - security: 189428 + security: 95864 securityOss: 30806 securitySolution: 187863 share: 99061 snapshotRestore: 79032 - spaces: 387915 + spaces: 57868 telemetry: 51957 telemetryManagementSection: 38586 tileMap: 65337 diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 0ecfc152197d3..e69006911e7f4 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -309,6 +309,9 @@ export class DocLinksService { }, apis: { bulkIndexAlias: `${ELASTICSEARCH_DOCS}indices-aliases.html`, + byteSizeUnits: `${ELASTICSEARCH_DOCS}common-options.html#byte-units`, + createAutoFollowPattern: `${ELASTICSEARCH_DOCS}ccr-put-auto-follow-pattern.html`, + createFollower: `${ELASTICSEARCH_DOCS}ccr-put-follow.html`, createIndex: `${ELASTICSEARCH_DOCS}indices-create-index.html`, createSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, createRoleMapping: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html`, @@ -329,6 +332,7 @@ export class DocLinksService { putSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, putWatch: `${ELASTICSEARCH_DOCS}watcher-api-put-watch.html`, simulatePipeline: `${ELASTICSEARCH_DOCS}simulate-pipeline-api.html`, + timeUnits: `${ELASTICSEARCH_DOCS}common-options.html#time-units`, updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`, }, plugins: { @@ -527,6 +531,9 @@ export interface DocLinksStart { readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; + byteSizeUnits: string; + createAutoFollowPattern: string; + createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; @@ -546,6 +553,7 @@ export interface DocLinksStart { putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; + timeUnits: string; updateTransform: string; }>; readonly observability: Record; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 574f37cb592e7..13660da598ea0 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -619,6 +619,9 @@ export interface DocLinksStart { readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; + byteSizeUnits: string; + createAutoFollowPattern: string; + createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; @@ -638,6 +641,7 @@ export interface DocLinksStart { putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; + timeUnits: string; updateTransform: string; }>; readonly observability: Record; diff --git a/src/dev/chromium_version.ts b/src/dev/chromium_version.ts index c49c8e1b812b5..410fcc72fbc0f 100644 --- a/src/dev/chromium_version.ts +++ b/src/dev/chromium_version.ts @@ -35,8 +35,10 @@ async function getPuppeteerRelease(log: ToolingLog): Promise { 'Could not get the Puppeteer version! Check node_modules/puppteer/package.json' ); } - log.info(`Kibana is using Puppeteer ${version} (${forkCompatibilityMap[version]})`); - return forkCompatibilityMap[version]; + const puppeteerRelease = forkCompatibilityMap[version] ?? version; + + log.info(`Kibana is using Puppeteer ${version} (${puppeteerRelease})`); + return puppeteerRelease; } async function getChromiumRevision( @@ -129,8 +131,8 @@ run( description: chalk` Display the Chromium git commit that correlates to a given Puppeteer release. - - node x-pack/dev-tools/chromium_version 5.5.0 {dim # gets the Chromium commit for Puppeteer v5.5.0} - - node x-pack/dev-tools/chromium_version {dim # gets the Chromium commit for the Kibana dependency version of Puppeteer} + - node scripts/chromium_version 5.5.0 {dim # gets the Chromium commit for Puppeteer v5.5.0} + - node scripts/chromium_version {dim # gets the Chromium commit for the Kibana dependency version of Puppeteer} You can use https://omahaproxy.appspot.com/ to look up the Chromium release that first shipped with that commit. `, diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index bb98498e6d601..fa82641e142d0 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -15,14 +15,14 @@ const log = new ToolingLog({ }); describe(`enumeratePatterns`, () => { - it(`should resolve x-pack/plugins/reporting/server/browsers/extract/unzip.js to kibana-reporting`, () => { + it(`should resolve x-pack/plugins/reporting/server/browsers/extract/unzip.ts to kibana-reporting`, () => { const actual = enumeratePatterns(REPO_ROOT)(log)( new Map([['x-pack/plugins/reporting', ['kibana-reporting']]]) ); expect( actual[0].includes( - 'x-pack/plugins/reporting/server/browsers/extract/unzip.js kibana-reporting' + 'x-pack/plugins/reporting/server/browsers/extract/unzip.ts kibana-reporting' ) ).toBe(true); }); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt index d8924bd563f30..15d3eb058bcf3 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt +++ b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt @@ -148,10 +148,10 @@ x-pack/plugins/reporting/server/browsers/download/download.ts kibana-reporting x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts kibana-reporting x-pack/plugins/reporting/server/browsers/download/index.ts kibana-reporting x-pack/plugins/reporting/server/browsers/download/util.ts kibana-reporting -x-pack/plugins/reporting/server/browsers/extract/extract.js kibana-reporting -x-pack/plugins/reporting/server/browsers/extract/extract_error.js kibana-reporting -x-pack/plugins/reporting/server/browsers/extract/index.js kibana-reporting -x-pack/plugins/reporting/server/browsers/extract/unzip.js kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/extract.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/extract_error.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/index.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/unzip.ts kibana-reporting x-pack/plugins/reporting/server/browsers/index.ts kibana-reporting x-pack/plugins/reporting/server/browsers/install.ts kibana-reporting x-pack/plugins/reporting/server/browsers/network_policy.test.ts kibana-reporting diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index 5ef3164f53822..60a68253d2c4e 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -32,13 +32,13 @@ describe(`Transform fns`, () => { it(`should remove the jenkins workspace path`, () => { const obj = { staticSiteUrl: - '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', + '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.ts', COVERAGE_INGESTION_KIBANA_ROOT: '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', }; expect(coveredFilePath(obj)).toHaveProperty( 'coveredFilePath', - 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' + 'x-pack/plugins/reporting/server/browsers/extract/unzip.ts' ); }); }); @@ -46,13 +46,13 @@ describe(`Transform fns`, () => { it(`should remove the jenkins workspace path`, () => { const obj = { staticSiteUrl: - '/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', + '/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.ts', COVERAGE_INGESTION_KIBANA_ROOT: '/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana', }; expect(coveredFilePath(obj)).toHaveProperty( 'coveredFilePath', - 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' + 'x-pack/plugins/reporting/server/browsers/extract/unzip.ts' ); }); }); @@ -82,7 +82,7 @@ describe(`Transform fns`, () => { describe(`teamAssignment`, () => { const teamAssignmentsPathMOCK = 'src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt'; - const coveredFilePath = 'x-pack/plugins/reporting/server/browsers/extract/unzip.js'; + const coveredFilePath = 'x-pack/plugins/reporting/server/browsers/extract/unzip.ts'; const obj = { coveredFilePath }; const log = new ToolingLog({ level: 'info', diff --git a/tsconfig.json b/tsconfig.json index 87ee067002109..b7122a70cb471 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "x-pack/typings/**/*", "x-pack/tasks/**/*", "x-pack/plugins/lists/**/*", - "x-pack/plugins/security_solution/**/*", + "x-pack/plugins/security_solution/**/*" ], "exclude": [ "x-pack/plugins/security_solution/cypress/**/*" @@ -110,6 +110,7 @@ { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, + { "path": "./x-pack/plugins/metrics_entities/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, { "path": "./x-pack/plugins/observability/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index b5e73e50f8b81..ab554a738b7a7 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -87,6 +87,7 @@ { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, + { "path": "./x-pack/plugins/metrics_entities/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, { "path": "./x-pack/plugins/observability/tsconfig.json" }, diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 99e33dbb88e92..9a02a9e552b40 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -3,7 +3,7 @@ /target /test/functional/failure_debug /test/functional/screenshots -/test/functional/apps/reporting/reports/session +/test/functional/apps/**/reports/session /test/reporting/configs/failure_debug/ /plugins/reporting/.chromium/ /plugins/reporting/chromium/ diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 4a03478800fc8..2db2f31ae09c3 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -39,6 +39,7 @@ "xpack.logstash": ["plugins/logstash"], "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": ["plugins/maps"], + "xpack.metricsEntities": "plugins/metrics_entities", "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], "xpack.osquery": ["plugins/osquery"], diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md index 0f4352671345d..b91f3f3a3c1ad 100644 --- a/x-pack/build_chromium/README.md +++ b/x-pack/build_chromium/README.md @@ -12,8 +12,8 @@ which is where we have two machines provisioned for the Linux and Windows builds. Mac builds can be achieved locally, and are a great place to start to gain familiarity. -**NOTE:** Linux builds should be done in Ubuntu on x86 architecture. ARM builds -are created in x86. CentOS is not supported for building Chromium. +**NOTE:** Linux builds should be done in Ubuntu on x64 architecture. ARM builds +are created in x64 using cross-compiling. CentOS is not supported for building Chromium. 1. Login to our GCP instance [here using your okta credentials](https://console.cloud.google.com/). 2. Click the "Compute Engine" tab. @@ -27,25 +27,32 @@ are created in x86. CentOS is not supported for building Chromium. - python2 (`python` must link to `python2`) - lsb_release - tmux is recommended in case your ssh session is interrupted -6. Copy the entire `build_chromium` directory into a GCP storage bucket, so you can copy the scripts into the instance and run them. + - "Cloud API access scopes": must have **read / write** scope for the Storage API +6. Copy the entire `build_chromium` directory from the `headless_shell_staging` bucket. To do this, use `gsutil rsync`: + ```sh + # This shows a preview of what would change by synchronizing the source scripts with the destination GCS bucket. + # Remove the `-n` flag to enact the changes + gsutil -m rsync -n -r x-pack/build_chromium gs://headless_shell_staging/build_chromium + ``` ## Build Script Usage -``` +These commands show how to set up an environment to build: +```sh # Allow our scripts to use depot_tools commands export PATH=$HOME/chromium/depot_tools:$PATH # Create a dedicated working directory for this directory of Python scripts. mkdir ~/chromium && cd ~/chromium -# Copy the scripts from the Kibana repo to use them conveniently in the working directory -gsutil cp -r gs://my-bucket/build_chromium . +# Copy the scripts from the Kibana team's GCS bucket +gsutil cp -r gs://headless_shell_staging/build_chromium . # Install the OS packages, configure the environment, download the chromium source (25GB) -python ./build_chromium/init.sh [arch_name] +python ./build_chromium/init.py [arch_name] # Run the build script with the path to the chromium src directory, the git commit hash -python ./build_chromium/build.py x86 +python ./build_chromium/build.py x64 # OR You can build for ARM python ./build_chromium/build.py arm64 @@ -107,7 +114,7 @@ use the Kibana `build.py` script (in this directory). It's recommended that you create a working directory for the chromium source code and all the build tools, and run the commands from there: -``` +```sh mkdir ~/chromium && cd ~/chromium cp -r ~/path/to/kibana/x-pack/build_chromium . python ./build_chromium/init.sh [arch_name] @@ -216,6 +223,7 @@ In the case of Windows, you can use IE to open `http://localhost:9221` and see i The following links provide helpful context about how the Chromium build works, and its prerequisites: +- Tools for Chromium version information: https://omahaproxy.appspot.com/ - https://www.chromium.org/developers/how-tos/get-the-code/working-with-release-branches - https://chromium.googlesource.com/chromium/src/+/HEAD/docs/windows_build_instructions.md - https://chromium.googlesource.com/chromium/src/+/HEAD/docs/mac_build_instructions.md diff --git a/x-pack/build_chromium/build.py b/x-pack/build_chromium/build.py index 0064f48ae973f..81e5f1f225ac5 100644 --- a/x-pack/build_chromium/build.py +++ b/x-pack/build_chromium/build.py @@ -3,9 +3,7 @@ from build_util import ( runcmd, runcmdsilent, - mkdir, md5_file, - configure_environment, ) # This file builds Chromium headless on Windows, Mac, and Linux. @@ -13,11 +11,10 @@ # Verify that we have an argument, and if not print instructions if (len(sys.argv) < 2): print('Usage:') - print('python build.py {chromium_version} [arch_name]') + print('python build.py {chromium_version} {arch_name}') print('Example:') - print('python build.py 68.0.3440.106') - print('python build.py 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479') - print('python build.py 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479 arm64 # build for ARM architecture') + print('python build.py 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479 x64') + print('python build.py 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479 arm64 # cross-compile for ARM architecture') print sys.exit(1) @@ -57,25 +54,34 @@ print('Creating a new branch for tracking the source version') runcmd('git checkout -b build-' + base_version + ' ' + source_version) +# configure environment: environment path depot_tools_path = os.path.join(build_path, 'depot_tools') -path_value = depot_tools_path + os.pathsep + os.environ['PATH'] -print('Updating PATH for depot_tools: ' + path_value) -os.environ['PATH'] = path_value +full_path = depot_tools_path + os.pathsep + os.environ['PATH'] +print('Updating PATH for depot_tools: ' + full_path) +os.environ['PATH'] = full_path + +# configure environment: build dependencies +if platform.system() == 'Linux': + if arch_name: + print('Running sysroot install script...') + runcmd(src_path + '/build/linux/sysroot_scripts/install-sysroot.py --arch=' + arch_name) + print('Running install-build-deps...') + runcmd(src_path + '/build/install-build-deps.sh') + + print('Updating all modules') -runcmd('gclient sync') +runcmd('gclient sync -D') -# Copy build args/{Linux | Darwin | Windows}.gn from the root of our directory to out/headless/args.gn, -argsgn_destination = path.abspath('out/headless/args.gn') -print('Generating platform-specific args') -mkdir('out/headless') -print(' > cp ' + argsgn_file + ' ' + argsgn_destination) -shutil.copyfile(argsgn_file, argsgn_destination) +print('Setting up build directory') +runcmd('rm -rf out/headless') +runcmd('mkdir out/headless') +# Copy build args/{Linux | Darwin | Windows}.gn from the root of our directory to out/headless/args.gn, +# add the target_cpu for cross-compilation print('Adding target_cpu to args') - -f = open('out/headless/args.gn', 'a') -f.write('\rtarget_cpu = "' + arch_name + '"\r') -f.close() +argsgn_file_out = path.abspath('out/headless/args.gn') +runcmd('cp ' + argsgn_file + ' ' + argsgn_file_out) +runcmd('echo \'target_cpu="' + arch_name + '"\' >> ' + argsgn_file_out) runcmd('gn gen out/headless') @@ -136,3 +142,6 @@ def archive_file(name): print('Creating ' + path.join(src_path, md5_filename)) with open (md5_filename, 'w') as f: f.write(md5_file(zip_filename)) + +runcmd('gsutil cp ' + path.join(src_path, zip_filename) + ' gs://headless_shell_staging') +runcmd('gsutil cp ' + path.join(src_path, md5_filename) + ' gs://headless_shell_staging') diff --git a/x-pack/build_chromium/build_util.py b/x-pack/build_chromium/build_util.py index eaa94e5170d5c..69298a798278d 100644 --- a/x-pack/build_chromium/build_util.py +++ b/x-pack/build_chromium/build_util.py @@ -27,19 +27,3 @@ def md5_file(filename): for chunk in iter(lambda: f.read(128 * md5.block_size), b''): md5.update(chunk) return md5.hexdigest() - -def configure_environment(arch_name, build_path, src_path): - """Runs install scripts for deps, and configures temporary environment variables required by Chromium's build""" - - if platform.system() == 'Linux': - if arch_name: - print('Running sysroot install script...') - sysroot_cmd = src_path + '/build/linux/sysroot_scripts/install-sysroot.py --arch=' + arch_name - runcmd(sysroot_cmd) - print('Running install-build-deps...') - runcmd(src_path + '/build/install-build-deps.sh') - - depot_tools_path = os.path.join(build_path, 'depot_tools') - full_path = depot_tools_path + os.pathsep + os.environ['PATH'] - print('Updating PATH for depot_tools: ' + full_path) - os.environ['PATH'] = full_path diff --git a/x-pack/build_chromium/darwin/args.gn b/x-pack/build_chromium/darwin/args.gn index 94009276e192b..58de679538398 100644 --- a/x-pack/build_chromium/darwin/args.gn +++ b/x-pack/build_chromium/darwin/args.gn @@ -19,8 +19,6 @@ use_alsa = false use_cups = false use_dbus = false use_gio = false -# Please, consult @elastic/kibana-security before changing/removing this option. -use_kerberos = false use_libpci = false use_pulseaudio = false use_udev = false @@ -28,4 +26,8 @@ use_udev = false is_debug = false symbol_level = 0 is_component_build = false -remove_webcore_debug_symbols = true + +# Please, consult @elastic/kibana-security before changing/removing this option. +use_kerberos = false + +# target_cpu is appended before build: "x64" or "arm64" diff --git a/x-pack/build_chromium/init.py b/x-pack/build_chromium/init.py index 3a2e28a884b09..eff9d3edb5fac 100644 --- a/x-pack/build_chromium/init.py +++ b/x-pack/build_chromium/init.py @@ -1,6 +1,6 @@ import os, platform, sys from os import path -from build_util import runcmd, mkdir, md5_file, configure_environment +from build_util import runcmd, mkdir # This is a cross-platform initialization script which should only be run # once per environment, and isn't intended to be run directly. You should @@ -44,6 +44,3 @@ runcmd('fetch chromium --nohooks=1 --no-history=1') else: print('Directory exists: ' + chromium_dir + '. Skipping chromium fetch.') - -# This depends on having the chromium/src directory with the complete checkout -configure_environment(arch_name, build_path, src_path) diff --git a/x-pack/build_chromium/linux/args.gn b/x-pack/build_chromium/linux/args.gn index 12b896aa0f618..fa6d4e8bcd15b 100644 --- a/x-pack/build_chromium/linux/args.gn +++ b/x-pack/build_chromium/linux/args.gn @@ -2,7 +2,8 @@ import("//build/args/headless.gn") is_debug = false symbol_level = 0 is_component_build = false -remove_webcore_debug_symbols = true enable_nacl = false # Please, consult @elastic/kibana-security before changing/removing this option. use_kerberos = false + +# target_cpu is appended before build: "x64" or "arm64" diff --git a/x-pack/build_chromium/windows/args.gn b/x-pack/build_chromium/windows/args.gn index de25eab7c7ff2..7b36a1194e2fb 100644 --- a/x-pack/build_chromium/windows/args.gn +++ b/x-pack/build_chromium/windows/args.gn @@ -18,10 +18,12 @@ use_gio = false use_libpci = false use_pulseaudio = false use_udev = false -# Please, consult @elastic/kibana-security before changing/removing this option. -use_kerberos = false is_debug = false symbol_level = 0 is_component_build = false -remove_webcore_debug_symbols = true + +# Please, consult @elastic/kibana-security before changing/removing this option. +use_kerberos = false + +# target_cpu is appended before build: "x64" or "arm64" diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index b1cfd59a37cec..17905074cfec1 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -34,6 +34,7 @@ import { import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; +import { UXActionMenu } from '../components/app/RumDashboard/ActionMenu'; const CsmMainContainer = euiStyled.div` padding: ${px(units.plus)}; @@ -104,6 +105,7 @@ export function CsmAppRoot({ + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx new file mode 100644 index 0000000000000..6d04996b5f24c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + createExploratoryViewUrl, + HeaderMenuPortal, + SeriesUrl, +} from '../../../../../../observability/public'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { AppMountParameters } from '../../../../../../../../src/core/public'; + +const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { + defaultMessage: 'Analyze data', +}); + +const ANALYZE_MESSAGE = i18n.translate( + 'xpack.apm.analyzeDataButtonLabel.message', + { + defaultMessage: + 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', + } +); + +export function UXActionMenu({ + appMountParameters, +}: { + appMountParameters: AppMountParameters; +}) { + const { + services: { http }, + } = useKibana(); + const { urlParams } = useUrlParams(); + const { rangeTo, rangeFrom } = urlParams; + + const uxExploratoryViewLink = createExploratoryViewUrl( + { + 'ux-series': { + dataType: 'ux', + time: { from: rangeFrom, to: rangeTo }, + } as SeriesUrl, + }, + http?.basePath.get() + ); + + return ( + + + + {ANALYZE_MESSAGE}

}> + + {ANALYZE_DATA} + +
+
+
+
+ ); +} diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js index 5b6b54135722c..41efd474e43dc 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js @@ -8,7 +8,9 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; +import { docLinksServiceMock } from '../../../../../../../src/core/public/mocks'; import { setHttpClient } from '../../../app/services/api'; +import { init as initDocumentation } from '../../../app/services/documentation_links'; import { init as initHttpRequests } from './http_requests'; export const setupEnvironment = () => { @@ -17,6 +19,7 @@ export const setupEnvironment = () => { const client = axios.create({ adapter: axiosXhrAdapter }); client.interceptors.response.use(({ data }) => data); setHttpClient(client); + initDocumentation(docLinksServiceMock.createStartContract()); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js index 45023811fd619..ada998ef37be8 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js @@ -18,7 +18,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { getAutoFollowPatternUrl } from '../services/documentation_links'; +import { documentationLinks } from '../services/documentation_links'; export const AutoFollowPatternPageTitle = ({ title }) => ( @@ -36,7 +36,7 @@ export const AutoFollowPatternPageTitle = ({ title }) => ( - - - ), - }} - /> -); +export const getAdvancedSettingsFields = (documentationLinks) => { + const byteUnitsHelpText = ( + + + + ), + }} + /> + ); -const timeUnitsHelpText = ( - - - - ), - }} - /> -); + const timeUnitsHelpText = ( + + + + ), + }} + /> + ); -export const advancedSettingsFields = [ - { - field: 'maxReadRequestOperationCount', - testSubject: 'maxReadRequestOperationCountInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountTitle', - { - defaultMessage: 'Max read request operation count', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountDescription', - { - defaultMessage: - 'The maximum number of operations to pull per read from the remote cluster.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountLabel', - { - defaultMessage: 'Max read request operation count', - } - ), - defaultValue: getSettingDefault('maxReadRequestOperationCount'), - type: 'number', - }, - { - field: 'maxOutstandingReadRequests', - testSubject: 'maxOutstandingReadRequestsInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsTitle', - { - defaultMessage: 'Max outstanding read requests', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsDescription', - { - defaultMessage: 'The maximum number of outstanding read requests from the remote cluster.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsLabel', - { - defaultMessage: 'Max outstanding read requests', - } - ), - defaultValue: getSettingDefault('maxOutstandingReadRequests'), - type: 'number', - }, - { - field: 'maxReadRequestSize', - testSubject: 'maxReadRequestSizeInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeTitle', - { - defaultMessage: 'Max read request size', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeDescription', - { - defaultMessage: - 'The maximum size in bytes of per read of a batch of operations pulled from the remote cluster.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeLabel', - { - defaultMessage: 'Max read request size', - } - ), - defaultValue: getSettingDefault('maxReadRequestSize'), - helpText: byteUnitsHelpText, - }, - { - field: 'maxWriteRequestOperationCount', - testSubject: 'maxWriteRequestOperationCountInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountTitle', - { - defaultMessage: 'Max write request operation count', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountDescription', - { - defaultMessage: - 'The maximum number of operations per bulk write request executed on the follower.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountLabel', - { - defaultMessage: 'Max write request operation count', - } - ), - defaultValue: getSettingDefault('maxWriteRequestOperationCount'), - type: 'number', - }, - { - field: 'maxWriteRequestSize', - testSubject: 'maxWriteRequestSizeInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeTitle', - { - defaultMessage: 'Max write request size', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeDescription', - { - defaultMessage: - 'The maximum total bytes of operations per bulk write request executed on the follower.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeLabel', - { - defaultMessage: 'Max write request size', - } - ), - defaultValue: getSettingDefault('maxWriteRequestSize'), - helpText: byteUnitsHelpText, - }, - { - field: 'maxOutstandingWriteRequests', - testSubject: 'maxOutstandingWriteRequestsInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsTitle', - { - defaultMessage: 'Max outstanding write requests', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsDescription', - { - defaultMessage: 'The maximum number of outstanding write requests on the follower.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsLabel', - { - defaultMessage: 'Max outstanding write requests', - } - ), - defaultValue: getSettingDefault('maxOutstandingWriteRequests'), - type: 'number', - }, - { - field: 'maxWriteBufferCount', - testSubject: 'maxWriteBufferCountInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountTitle', - { - defaultMessage: 'Max write buffer count', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountDescription', - { - defaultMessage: - 'The maximum number of operations that can be queued for writing; when this ' + - 'limit is reached, reads from the remote cluster will be deferred until the number of queued ' + - 'operations goes below the limit.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountLabel', - { - defaultMessage: 'Max write buffer count', - } - ), - defaultValue: getSettingDefault('maxWriteBufferCount'), - type: 'number', - }, - { - field: 'maxWriteBufferSize', - testSubject: 'maxWriteBufferSizeInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeTitle', - { - defaultMessage: 'Max write buffer size', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeDescription', - { - defaultMessage: - 'The maximum total bytes of operations that can be queued for writing; when ' + - 'this limit is reached, reads from the remote cluster will be deferred until the total bytes ' + - 'of queued operations goes below the limit.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeLabel', - { - defaultMessage: 'Max write buffer size', - } - ), - defaultValue: getSettingDefault('maxWriteBufferSize'), - helpText: byteUnitsHelpText, - }, - { - field: 'maxRetryDelay', - testSubject: 'maxRetryDelayInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayTitle', - { - defaultMessage: 'Max retry delay', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayDescription', - { - defaultMessage: - 'The maximum time to wait before retrying an operation that failed exceptionally; ' + - 'an exponential backoff strategy is employed when retrying.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayLabel', - { - defaultMessage: 'Max retry delay', - } - ), - defaultValue: getSettingDefault('maxRetryDelay'), - helpText: timeUnitsHelpText, - }, - { - field: 'readPollTimeout', - testSubject: 'readPollTimeoutInput', - title: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutTitle', - { - defaultMessage: 'Read poll timeout', - } - ), - description: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutDescription', - { - defaultMessage: - 'The maximum time to wait for new operations on the remote cluster when the ' + - 'follower index is synchronized with the leader index; when the timeout has elapsed, the ' + - 'poll for operations will return to the follower so that it can update some statistics, and ' + - 'then the follower will immediately attempt to read from the leader again.', - } - ), - label: i18n.translate( - 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutLabel', - { - defaultMessage: 'Read poll timeout', - } - ), - defaultValue: getSettingDefault('readPollTimeout'), - helpText: timeUnitsHelpText, - }, -]; + return [ + { + field: 'maxReadRequestOperationCount', + testSubject: 'maxReadRequestOperationCountInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountTitle', + { + defaultMessage: 'Max read request operation count', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountDescription', + { + defaultMessage: + 'The maximum number of operations to pull per read from the remote cluster.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountLabel', + { + defaultMessage: 'Max read request operation count', + } + ), + defaultValue: getSettingDefault('maxReadRequestOperationCount'), + type: 'number', + }, + { + field: 'maxOutstandingReadRequests', + testSubject: 'maxOutstandingReadRequestsInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsTitle', + { + defaultMessage: 'Max outstanding read requests', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsDescription', + { + defaultMessage: + 'The maximum number of outstanding read requests from the remote cluster.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsLabel', + { + defaultMessage: 'Max outstanding read requests', + } + ), + defaultValue: getSettingDefault('maxOutstandingReadRequests'), + type: 'number', + }, + { + field: 'maxReadRequestSize', + testSubject: 'maxReadRequestSizeInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeTitle', + { + defaultMessage: 'Max read request size', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeDescription', + { + defaultMessage: + 'The maximum size in bytes of per read of a batch of operations pulled from the remote cluster.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeLabel', + { + defaultMessage: 'Max read request size', + } + ), + defaultValue: getSettingDefault('maxReadRequestSize'), + helpText: byteUnitsHelpText, + }, + { + field: 'maxWriteRequestOperationCount', + testSubject: 'maxWriteRequestOperationCountInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountTitle', + { + defaultMessage: 'Max write request operation count', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountDescription', + { + defaultMessage: + 'The maximum number of operations per bulk write request executed on the follower.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountLabel', + { + defaultMessage: 'Max write request operation count', + } + ), + defaultValue: getSettingDefault('maxWriteRequestOperationCount'), + type: 'number', + }, + { + field: 'maxWriteRequestSize', + testSubject: 'maxWriteRequestSizeInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeTitle', + { + defaultMessage: 'Max write request size', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeDescription', + { + defaultMessage: + 'The maximum total bytes of operations per bulk write request executed on the follower.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeLabel', + { + defaultMessage: 'Max write request size', + } + ), + defaultValue: getSettingDefault('maxWriteRequestSize'), + helpText: byteUnitsHelpText, + }, + { + field: 'maxOutstandingWriteRequests', + testSubject: 'maxOutstandingWriteRequestsInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsTitle', + { + defaultMessage: 'Max outstanding write requests', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsDescription', + { + defaultMessage: 'The maximum number of outstanding write requests on the follower.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsLabel', + { + defaultMessage: 'Max outstanding write requests', + } + ), + defaultValue: getSettingDefault('maxOutstandingWriteRequests'), + type: 'number', + }, + { + field: 'maxWriteBufferCount', + testSubject: 'maxWriteBufferCountInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountTitle', + { + defaultMessage: 'Max write buffer count', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountDescription', + { + defaultMessage: + 'The maximum number of operations that can be queued for writing; when this ' + + 'limit is reached, reads from the remote cluster will be deferred until the number of queued ' + + 'operations goes below the limit.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountLabel', + { + defaultMessage: 'Max write buffer count', + } + ), + defaultValue: getSettingDefault('maxWriteBufferCount'), + type: 'number', + }, + { + field: 'maxWriteBufferSize', + testSubject: 'maxWriteBufferSizeInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeTitle', + { + defaultMessage: 'Max write buffer size', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeDescription', + { + defaultMessage: + 'The maximum total bytes of operations that can be queued for writing; when ' + + 'this limit is reached, reads from the remote cluster will be deferred until the total bytes ' + + 'of queued operations goes below the limit.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeLabel', + { + defaultMessage: 'Max write buffer size', + } + ), + defaultValue: getSettingDefault('maxWriteBufferSize'), + helpText: byteUnitsHelpText, + }, + { + field: 'maxRetryDelay', + testSubject: 'maxRetryDelayInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayTitle', + { + defaultMessage: 'Max retry delay', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayDescription', + { + defaultMessage: + 'The maximum time to wait before retrying an operation that failed exceptionally; ' + + 'an exponential backoff strategy is employed when retrying.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayLabel', + { + defaultMessage: 'Max retry delay', + } + ), + defaultValue: getSettingDefault('maxRetryDelay'), + helpText: timeUnitsHelpText, + }, + { + field: 'readPollTimeout', + testSubject: 'readPollTimeoutInput', + title: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutTitle', + { + defaultMessage: 'Read poll timeout', + } + ), + description: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutDescription', + { + defaultMessage: + 'The maximum time to wait for new operations on the remote cluster when the ' + + 'follower index is synchronized with the leader index; when the timeout has elapsed, the ' + + 'poll for operations will return to the follower so that it can update some statistics, and ' + + 'then the follower will immediately attempt to read from the leader again.', + } + ), + label: i18n.translate( + 'xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutLabel', + { + defaultMessage: 'Read poll timeout', + } + ), + defaultValue: getSettingDefault('readPollTimeout'), + helpText: timeUnitsHelpText, + }, + ]; +}; -export const emptyAdvancedSettings = advancedSettingsFields.reduce((obj, advancedSetting) => { - const { field, defaultValue } = advancedSetting; - return { ...obj, [field]: defaultValue }; -}, {}); +export const getEmptyAdvancedSettings = (documentationLinks) => + getAdvancedSettingsFields(documentationLinks).reduce((obj, advancedSetting) => { + const { field, defaultValue } = advancedSetting; + return { ...obj, [field]: defaultValue }; + }, {}); -export function areAdvancedSettingsEdited(followerIndex) { - return advancedSettingsFields.some((advancedSetting) => { +export function areAdvancedSettingsEdited(followerIndex, documentationLinks) { + return getAdvancedSettingsFields(documentationLinks).some((advancedSetting) => { const { field } = advancedSetting; - return followerIndex[field] !== emptyAdvancedSettings[field]; + return followerIndex[field] !== getEmptyAdvancedSettings(documentationLinks)[field]; }); } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js index bca4ec702a5b5..dc117a9cd4581 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js @@ -34,14 +34,15 @@ import { indexNameValidator, leaderIndexValidator } from '../../services/input_v import { routing } from '../../services/routing'; import { getFatalErrors } from '../../services/notifications'; import { loadIndices } from '../../services/api'; +import { documentationLinks } from '../../services/documentation_links'; import { API_STATUS } from '../../constants'; import { getRemoteClusterName } from '../../services/get_remote_cluster_name'; import { RemoteClustersFormField } from '../remote_clusters_form_field'; import { SectionError } from '../section_error'; import { FormEntryRow } from '../form_entry_row'; import { - advancedSettingsFields, - emptyAdvancedSettings, + getAdvancedSettingsFields, + getEmptyAdvancedSettings, areAdvancedSettingsEdited, } from './advanced_settings_fields'; @@ -49,23 +50,24 @@ import { FollowerIndexRequestFlyout } from './follower_index_request_flyout'; const indexNameIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' '); -const fieldToValidatorMap = advancedSettingsFields.reduce( - (map, advancedSetting) => { - const { field, validator } = advancedSetting; - map[field] = validator; - return map; - }, - { - name: indexNameValidator, - leaderIndex: leaderIndexValidator, - } -); +const getFieldToValidatorMap = (advancedSettingsFields) => + advancedSettingsFields.reduce( + (map, advancedSetting) => { + const { field, validator } = advancedSetting; + map[field] = validator; + return map; + }, + { + name: indexNameValidator, + leaderIndex: leaderIndexValidator, + } + ); const getEmptyFollowerIndex = (remoteClusterName = '') => ({ name: '', remoteCluster: remoteClusterName, leaderIndex: '', - ...emptyAdvancedSettings, + ...getEmptyAdvancedSettings(documentationLinks), }); /** @@ -121,7 +123,7 @@ export class FollowerIndexForm extends PureComponent { // eslint-disable-next-line no-nested-ternary const areAdvancedSettingsVisible = isNew ? false - : areAdvancedSettingsEdited(followerIndex) + : areAdvancedSettingsEdited(followerIndex, documentationLinks) ? true : false; @@ -164,7 +166,8 @@ export class FollowerIndexForm extends PureComponent { getFieldsErrors = (newFields) => { return Object.keys(newFields).reduce((errors, field) => { - const validator = fieldToValidatorMap[field]; + const advancedSettings = getAdvancedSettingsFields(documentationLinks); + const validator = getFieldToValidatorMap(advancedSettings)[field]; const value = newFields[field]; if (validator) { @@ -278,17 +281,20 @@ export class FollowerIndexForm extends PureComponent { } // Clear the advanced settings form. - this.onFieldsChange(emptyAdvancedSettings); + this.onFieldsChange(getEmptyAdvancedSettings(documentationLinks)); // Save a cache of the advanced settings. const fields = this.getFields(); - this.cachedAdvancedSettings = advancedSettingsFields.reduce((cache, { field }) => { - const value = fields[field]; - if (value !== '') { - cache[field] = value; - } - return cache; - }, {}); + this.cachedAdvancedSettings = getAdvancedSettingsFields(documentationLinks).reduce( + (cache, { field }) => { + const value = fields[field]; + if (value !== '') { + cache[field] = value; + } + return cache; + }, + {} + ); // Hide the advanced settings. this.setState({ @@ -614,7 +620,7 @@ export class FollowerIndexForm extends PureComponent { {areAdvancedSettingsVisible && ( - {advancedSettingsFields.map((advancedSetting) => { + {getAdvancedSettingsFields(documentationLinks).map((advancedSetting) => { const { field, testSubject, diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js index afc8892352132..b5652d3f2b6e6 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js @@ -18,7 +18,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { getFollowerIndexUrl } from '../services/documentation_links'; +import { documentationLinks } from '../services/documentation_links'; export const FollowerIndexPageTitle = ({ title }) => ( @@ -36,7 +36,7 @@ export const FollowerIndexPageTitle = ({ title }) => ( { // Import and initialize additional services here instead of in plugin.ts to reduce the size of the // initial bundle as much as possible. initBreadcrumbs(setBreadcrumbs); - initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`); + initDocumentation(docLinks); return renderApp(element, I18nContext, history, getUrlForApp); } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts index 25c92bbcdcad6..65bbfd919f94d 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts @@ -5,13 +5,10 @@ * 2.0. */ -let _esBase: string; +import type { DocLinksStart } from 'src/core/public'; -export const init = (esBase: string) => { - _esBase = esBase; -}; +export let documentationLinks: DocLinksStart['links']; -export const getAutoFollowPatternUrl = (): string => `${_esBase}/ccr-put-auto-follow-pattern.html`; -export const getFollowerIndexUrl = (): string => `${_esBase}/ccr-put-follow.html`; -export const getByteUnitsUrl = (): string => `${_esBase}/common-options.html#byte-units`; -export const getTimeUnitsUrl = (): string => `${_esBase}/common-options.html#time-units`; +export const init = (docLinks: DocLinksStart) => { + documentationLinks = docLinks.links; +}; diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts index 7998cdbdf750b..a45862d46beeb 100644 --- a/x-pack/plugins/cross_cluster_replication/public/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -48,7 +48,7 @@ export class CrossClusterReplicationPlugin implements Plugin { const { chrome: { docTitle }, i18n: { Context: I18nContext }, - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + docLinks, application: { getUrlForApp }, } = coreStart; @@ -58,8 +58,7 @@ export class CrossClusterReplicationPlugin implements Plugin { element, setBreadcrumbs, I18nContext, - ELASTIC_WEBSITE_URL, - DOC_LINK_VERSION, + docLinks, history, getUrlForApp, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index add5e9414be13..565c3069788c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -15,7 +15,7 @@ import { mockEngineValues } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; -import { InternalSchemaTypes } from '../../../shared/types'; +import { InternalSchemaType } from '../../../shared/schema/types'; import { DocumentDetailLogic } from './document_detail_logic'; @@ -38,7 +38,7 @@ describe('DocumentDetailLogic', () => { describe('actions', () => { describe('setFields', () => { it('should set fields to the provided value and dataLoading to false', () => { - const fields = [{ name: 'foo', value: ['foo'], type: 'string' as InternalSchemaTypes }]; + const fields = [{ name: 'foo', value: ['foo'], type: InternalSchemaType.String }]; mount({ dataLoading: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts index 8b7e575ae031b..472a37e158062 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SchemaTypes } from '../../../../shared/types'; +import { SchemaType } from '../../../../shared/schema/types'; import { buildSearchUIConfig } from './build_search_ui_config'; @@ -13,8 +13,8 @@ describe('buildSearchUIConfig', () => { it('builds a configuration object for Search UI', () => { const connector = {}; const schema = { - foo: 'text' as SchemaTypes, - bar: 'number' as SchemaTypes, + foo: SchemaType.Text, + bar: SchemaType.Number, }; const fields = { filterFields: ['fieldA', 'fieldB'], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index 9fac068555db5..25342f24cc872 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Schema } from '../../../../shared/types'; +import { Schema } from '../../../../shared/schema/types'; import { Fields } from './types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 0905000f55139..ea111402309b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -15,7 +15,7 @@ import { shallow } from 'enzyme'; // @ts-expect-error types are not available for this package yet import { Results } from '@elastic/react-search-ui'; -import { SchemaTypes } from '../../../../shared/types'; +import { SchemaType } from '../../../../shared/schema/types'; import { Pagination } from './pagination'; import { SearchExperienceContent } from './search_experience_content'; @@ -33,7 +33,7 @@ describe('SearchExperienceContent', () => { myRole: { canManageEngineDocuments: true }, engine: { schema: { - title: 'string' as SchemaTypes, + title: SchemaType.Text, }, }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx index 24685aef71078..0e7c3c57c41ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SchemaTypes } from '../../../../../shared/types'; +import { SchemaType } from '../../../../../shared/schema/types'; import { Result } from '../../../result/result'; import { ResultView } from '.'; @@ -30,7 +30,7 @@ describe('ResultView', () => { }; const schema = { - title: 'string' as SchemaTypes, + title: SchemaType.Text, }; it('renders', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx index b133780310a4c..45dafe385f737 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { Schema } from '../../../../../shared/types'; +import { Schema } from '../../../../../shared/schema/types'; import { Result } from '../../../result/result'; import { Result as ResultType } from '../../../result/types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts index 75488ea16f86c..a6b4a307b9c5f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { InternalSchemaTypes } from '../../../shared/types'; +import { InternalSchemaType } from '../../../shared/schema/types'; export interface FieldDetails { name: string; value: string | string[]; - type: InternalSchemaTypes; + type: InternalSchemaType; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index aa4a978da0550..768a9e545b878 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -8,8 +8,7 @@ import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; - -import { IIndexingStatus } from '../../../shared/types'; +import { IIndexingStatus } from '../../../shared/schema/types'; import { EngineDetails, EngineTypes } from './types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 7d8c1b420378f..530accb501c41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -124,8 +124,8 @@ export const EngineNav: React.FC = () => { )} {canViewEngineSchema && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index ba9173e54ec08..39055e772bcf9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -23,6 +23,7 @@ import { Documents, DocumentDetail } from '../documents'; import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; +import { SchemaRouter } from '../schema'; import { SearchUI } from '../search_ui'; import { SourceEngines } from '../source_engines'; import { Synonyms } from '../synonyms'; @@ -112,6 +113,13 @@ describe('EngineRouter', () => { expect(wrapper.find(DocumentDetail)).toHaveLength(1); }); + it('renders a schema view', () => { + setMockValues({ ...values, myRole: { canViewEngineSchema: true } }); + const wrapper = shallow(); + + expect(wrapper.find(SchemaRouter)).toHaveLength(1); + }); + it('renders a synonyms view', () => { setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); const wrapper = shallow(); 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 65769446b10db..387f8cf1b9837 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 @@ -22,7 +22,7 @@ import { ENGINE_ANALYTICS_PATH, ENGINE_DOCUMENTS_PATH, ENGINE_DOCUMENT_DETAIL_PATH, - // ENGINE_SCHEMA_PATH, + ENGINE_SCHEMA_PATH, // ENGINE_CRAWLER_PATH, META_ENGINE_SOURCE_ENGINES_PATH, ENGINE_RELEVANCE_TUNING_PATH, @@ -39,6 +39,7 @@ import { DocumentDetail, Documents } from '../documents'; import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; +import { SchemaRouter } from '../schema'; import { SearchUI } from '../search_ui'; import { SourceEngines } from '../source_engines'; import { Synonyms } from '../synonyms'; @@ -50,7 +51,7 @@ export const EngineRouter: React.FC = () => { myRole: { canViewEngineAnalytics, canViewEngineDocuments, - // canViewEngineSchema, + canViewEngineSchema, // canViewEngineCrawler, canViewMetaEngineSourceEngines, canManageEngineRelevanceTuning, @@ -102,6 +103,11 @@ export const EngineRouter: React.FC = () => { )} + {canViewEngineSchema && ( + + + + )} {canManageEngineCurations && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index 75828fa9bfc4c..2c22a3addf63b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Schema, SchemaConflicts, IIndexingStatus } from '../../../shared/types'; +import { Schema, SchemaConflicts, IIndexingStatus } from '../../../shared/schema/types'; import { ApiToken } from '../credentials/types'; export enum EngineTypes { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_name_column_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_name_column_content.test.tsx index df65f2f86e174..bad55b2542c70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_name_column_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_name_column_content.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { EuiHealth } from '@elastic/eui'; -import { SchemaConflictFieldTypes, SchemaConflicts } from '../../../../../shared/types'; +import { SchemaConflictFieldTypes, SchemaConflicts } from '../../../../../shared/schema/types'; import { EngineDetails } from '../../../engine/types'; import { MetaEnginesTableNameColumnContent } from './meta_engines_table_name_column_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.test.ts index f65a2e52bae06..2fbf108223fb0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SchemaConflictFieldTypes, SchemaConflicts } from '../../../../../shared/types'; +import { SchemaConflictFieldTypes, SchemaConflicts } from '../../../../../shared/schema/types'; import { EngineDetails } from '../../../engine/types'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.ts index b1172237e3ad3..d9f41f8558b78 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SchemaConflictFieldTypes, SchemaConflicts } from '../../../../../shared/types'; +import { SchemaConflictFieldTypes, SchemaConflicts } from '../../../../../shared/schema/types'; import { EngineDetails } from '../../../engine/types'; export const getConflictingEnginesFromConflictingField = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 9ad32c6e48632..5d61929770299 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Schema } from '../../../shared/types'; +import { Schema, SchemaType } from '../../../shared/schema/types'; import { Result } from '../result'; export const Library: React.FC = () => { @@ -63,14 +63,14 @@ export const Library: React.FC = () => { }; const schema: Schema = { - title: 'text', - description: 'text', - date_established: 'date', - location: 'geolocation', - states: 'text', - visitors: 'number', - size: 'number', - length: 'number', + title: SchemaType.Text, + description: SchemaType.Text, + date_established: SchemaType.Date, + location: SchemaType.Geolocation, + states: SchemaType.Text, + visitors: SchemaType.Number, + size: SchemaType.Number, + length: SchemaType.Number, }; const [isActionButtonFilled, setIsActionButtonFilled] = useState(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx index 897639fe9e6bc..c82efa906f676 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx @@ -13,7 +13,7 @@ import { shallow } from 'enzyme'; import { EuiSuperSelect } from '@elastic/eui'; -import { SchemaTypes } from '../../../../shared/types'; +import { SchemaType } from '../../../../shared/schema/types'; import { BoostType } from '../types'; @@ -35,7 +35,7 @@ describe('Boosts', () => { const props = { name: 'foo', - type: 'number' as SchemaTypes, + type: SchemaType.Number, }; it('renders a select box that allows users to create boosts of various types', () => { @@ -55,7 +55,7 @@ describe('Boosts', () => { ); @@ -69,7 +69,7 @@ describe('Boosts', () => { ); @@ -83,7 +83,7 @@ describe('Boosts', () => { ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx index 7a407491ffef3..16249f8a9b370 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx @@ -13,8 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@ import { i18n } from '@kbn/i18n'; -import { GEOLOCATION, TEXT, DATE } from '../../../../shared/constants/field_types'; -import { SchemaTypes } from '../../../../shared/types'; +import { SchemaType } from '../../../../shared/schema/types'; import { BoostIcon } from '../components'; import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../constants'; @@ -65,19 +64,21 @@ const BASE_OPTIONS = [ }, ]; -const filterInvalidOptions = (value: BoostType, type: SchemaTypes) => { +const filterInvalidOptions = (value: BoostType, type: SchemaType) => { // Proximity and Functional boost types are not valid for text fields - if (type === TEXT && [BoostType.Proximity, BoostType.Functional].includes(value)) return false; + if (type === SchemaType.Text && [BoostType.Proximity, BoostType.Functional].includes(value)) + return false; // Value and Functional boost types are not valid for geolocation fields - if (type === GEOLOCATION && [BoostType.Functional, BoostType.Value].includes(value)) return false; + if (type === SchemaType.Geolocation && [BoostType.Functional, BoostType.Value].includes(value)) + return false; // Functional boosts are not valid for date fields - if (type === DATE && value === BoostType.Functional) return false; + if (type === SchemaType.Date && value === BoostType.Functional) return false; return true; }; interface Props { name: string; - type: SchemaTypes; + type: SchemaType; boosts?: Boost[]; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx index b6061a326365b..1d813cfc8f6a0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SchemaTypes } from '../../../../shared/types'; +import { SchemaType } from '../../../../shared/schema/types'; import { BoostIcon, ValueBadge } from '../components'; import { Boost, BoostType, SearchField } from '../types'; @@ -19,7 +19,7 @@ import { RelevanceTuningItem } from './relevance_tuning_item'; describe('RelevanceTuningItem', () => { const props = { name: 'foo', - type: 'text' as SchemaTypes, + type: SchemaType.Text, boosts: [ { factor: 2, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx index 9264078ca40f5..f6f5135792141 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx @@ -9,14 +9,14 @@ import React from 'react'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiTextColor, EuiIcon } from '@elastic/eui'; -import { SchemaTypes } from '../../../../shared/types'; +import { SchemaType } from '../../../../shared/schema/types'; import { BoostIcon, ValueBadge } from '../components'; import { Boost, SearchField } from '../types'; interface Props { name: string; - type: SchemaTypes; + type: SchemaType; boosts?: Boost[]; field?: SearchField; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx index 65a42216e17ff..9b3003a192107 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SchemaTypes } from '../../../../../shared/types'; +import { SchemaType } from '../../../../../shared/schema/types'; import { BoostType } from '../../types'; import { RelevanceTuningItemContent } from './relevance_tuning_item_content'; @@ -19,7 +19,7 @@ import { WeightSlider } from './weight_slider'; describe('RelevanceTuningItemContent', () => { const props = { name: 'foo', - type: 'text' as SchemaTypes, + type: SchemaType.Text, boosts: [ { factor: 2, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx index e780a4de07252..18bce47b18ae4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; -import { SchemaTypes } from '../../../../../shared/types'; +import { SchemaType } from '../../../../../shared/schema/types'; import { Boosts } from '../../boosts'; import { Boost, SearchField } from '../../types'; @@ -19,7 +19,7 @@ import { WeightSlider } from './weight_slider'; interface Props { name: string; - type: SchemaTypes; + type: SchemaType; boosts?: Boost[]; field?: SearchField; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx index 7225fce5daa61..f2d4f9c20a58d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx @@ -13,7 +13,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiSwitch } from '@elastic/eui'; -import { SchemaTypes } from '../../../../../shared/types'; +import { SchemaType } from '../../../../../shared/schema/types'; import { TextSearchToggle } from './text_search_toggle'; @@ -35,7 +35,7 @@ describe('TextSearchToggle', () => { const props = { name: 'foo', - type: 'text' as SchemaTypes, + type: SchemaType.Text, field: { weight: 1, }, @@ -72,7 +72,7 @@ describe('TextSearchToggle', () => { const props = { name: 'foo', - type: 'number' as SchemaTypes, + type: SchemaType.Number, field: { weight: 1, }, @@ -103,7 +103,7 @@ describe('TextSearchToggle', () => { const props = { name: 'foo', - type: 'text' as SchemaTypes, + type: SchemaType.Text, }; beforeAll(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx index 607ddd9c6b078..937e4dc9f2daa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx @@ -13,20 +13,20 @@ import { EuiFormRow, EuiSwitch } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { TEXT } from '../../../../../shared/constants/field_types'; -import { SchemaTypes } from '../../../../../shared/types'; +import { SchemaType } from '../../../../../shared/schema/types'; import { RelevanceTuningLogic } from '../../relevance_tuning_logic'; import { SearchField } from '../../types'; interface Props { name: string; - type: SchemaTypes; + type: SchemaType; field?: SearchField; } export const TextSearchToggle: React.FC = ({ name, type, field }) => { const { toggleSearchField } = useActions(RelevanceTuningLogic); + const isText = type === SchemaType.Text; return ( = ({ name, type, field }) => { > = ({ name, type, field }) => { } ) } - onChange={() => type === TEXT && toggleSearchField(name, !!field)} + onChange={() => isText && toggleSearchField(name, !!field)} checked={!!field} - disabled={type !== TEXT} + disabled={!isText} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index 4787ef89c0119..b3c795d14b8e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -14,7 +14,7 @@ import { clearFlashMessages, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; -import { Schema, SchemaConflicts } from '../../../shared/types'; +import { Schema, SchemaConflicts } from '../../../shared/schema/types'; import { EngineLogic } from '../engine'; import { Result } from '../result/types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts index b5df8bf0c667a..20380717a4074 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts @@ -4,6 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { SchemaType } from '../../../shared/schema/types'; + import { Boost, BoostType } from './types'; import { filterIfTerm, @@ -70,17 +73,17 @@ describe('removeBoostStateProps', () => { describe('parseBoostCenter', () => { it('should parse the value to a number when the type is number', () => { - expect(parseBoostCenter('number', 5)).toEqual(5); - expect(parseBoostCenter('number', '5')).toEqual(5); + expect(parseBoostCenter(SchemaType.Number, 5)).toEqual(5); + expect(parseBoostCenter(SchemaType.Number, '5')).toEqual(5); }); it('should not try to parse the value when the type is text', () => { - expect(parseBoostCenter('text', 5)).toEqual(5); - expect(parseBoostCenter('text', '4')).toEqual('4'); + expect(parseBoostCenter(SchemaType.Text, 5)).toEqual(5); + expect(parseBoostCenter(SchemaType.Text, '4')).toEqual('4'); }); it('should leave text invalid numbers alone', () => { - expect(parseBoostCenter('number', 'foo')).toEqual('foo'); + expect(parseBoostCenter(SchemaType.Number, 'foo')).toEqual('foo'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts index 5aaab80778e02..be953f973ebf8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts @@ -7,8 +7,7 @@ import { cloneDeep, omit } from 'lodash'; -import { NUMBER } from '../../../shared/constants/field_types'; -import { SchemaTypes } from '../../../shared/types'; +import { SchemaType } from '../../../shared/schema/types'; import { RawBoost, Boost, SearchSettings, BoostType, ValueBoost } from './types'; @@ -26,9 +25,9 @@ export const removeBoostStateProps = (searchSettings: SearchSettings) => { return updatedSettings; }; -export const parseBoostCenter = (fieldType: SchemaTypes, value: string | number) => { +export const parseBoostCenter = (fieldType: SchemaType, value: string | number) => { // Leave non-numeric fields alone - if (fieldType === NUMBER) { + if (fieldType === SchemaType.Number) { const floatValue = parseFloat(value as string); return isNaN(floatValue) ? value : floatValue; } 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 ba9944744e5c7..333cefecb99c8 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 @@ -14,7 +14,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiPanel } from '@elastic/eui'; -import { SchemaTypes } from '../../../shared/types'; +import { SchemaType } from '../../../shared/schema/types'; import { Result } from './result'; import { ResultField } from './result_field'; @@ -45,9 +45,9 @@ describe('Result', () => { }; const schema = { - title: 'text' as SchemaTypes, - description: 'text' as SchemaTypes, - length: 'number' as SchemaTypes, + title: SchemaType.Text, + description: SchemaType.Text, + length: SchemaType.Number, }; it('renders', () => { 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 d9c16a877dc59..9be9afa051351 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 @@ -15,7 +15,7 @@ import { EuiPanel, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { KibanaLogic } from '../../../shared/kibana'; -import { Schema } from '../../../shared/types'; +import { Schema } from '../../../shared/schema/types'; import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; import { generateEncodedPath } from '../../utils/encode_path_params'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx index 1e79266dd7e7d..d8586c3fb3518 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { InternalSchemaType } from '../../../shared/schema/types'; + import { ResultField } from './result_field'; describe('ResultField', () => { @@ -18,7 +20,7 @@ describe('ResultField', () => { field="title" raw="The Catcher in the Rye" snippet="The Catcher in the Rye" - type="string" + type={InternalSchemaType.String} /> ); expect(wrapper.find('ResultFieldValue').exists()).toBe(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx index c732c9c8216c0..ebb2c10748d87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx @@ -9,13 +9,15 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { SchemaType, InternalSchemaType } from '../../../shared/schema/types'; + import { ResultFieldValue } from '.'; describe('ResultFieldValue', () => { describe('when no raw or snippet values are provided', () => { let wrapper: ShallowWrapper; beforeAll(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('will render a dash', () => { @@ -27,7 +29,7 @@ describe('ResultFieldValue', () => { describe('and the value is a string', () => { let wrapper: ShallowWrapper; beforeAll(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('will render a display value', () => { @@ -35,14 +37,16 @@ describe('ResultFieldValue', () => { }); it('will have the appropriate type class', () => { - expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--string'); + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--text'); }); }); describe('and the value is a string array', () => { let wrapper: ShallowWrapper; beforeAll(() => { - wrapper = shallow(); + wrapper = shallow( + + ); }); it('will render a display value', () => { @@ -57,7 +61,7 @@ describe('ResultFieldValue', () => { describe('and the value is a number', () => { let wrapper: ShallowWrapper; beforeAll(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('will render a display value', () => { @@ -72,7 +76,7 @@ describe('ResultFieldValue', () => { describe('and the value is an array of numbers', () => { let wrapper: ShallowWrapper; beforeAll(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('will render a display value', () => { @@ -80,14 +84,14 @@ describe('ResultFieldValue', () => { }); it('will have the appropriate type class', () => { - expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--number'); + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--float'); }); }); describe('and the value is a location', () => { let wrapper: ShallowWrapper; beforeAll(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('will render a display value', () => { @@ -95,7 +99,7 @@ describe('ResultFieldValue', () => { }); it('will have the appropriate type class', () => { - expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--location'); + expect(wrapper.prop('className')).toContain('enterpriseSearchDataType--geolocation'); }); }); @@ -103,7 +107,10 @@ describe('ResultFieldValue', () => { let wrapper: ShallowWrapper; beforeAll(() => { wrapper = shallow( - + ); }); @@ -119,7 +126,7 @@ describe('ResultFieldValue', () => { describe('and the value is a date', () => { let wrapper: ShallowWrapper; beforeAll(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('will render a display value', () => { @@ -135,7 +142,10 @@ describe('ResultFieldValue', () => { let wrapper: ShallowWrapper; beforeAll(() => { wrapper = shallow( - + ); }); @@ -156,7 +166,7 @@ describe('ResultFieldValue', () => { ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.test.tsx index d50b35198acb9..a35b373371a66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.test.tsx @@ -11,6 +11,8 @@ import { shallow } from 'enzyme'; import { EuiToken } from '@elastic/eui'; +import { SchemaType } from '../../../shared/schema/types'; + import { ResultToken } from './result_token'; describe('ResultToken', () => { @@ -20,7 +22,7 @@ describe('ResultToken', () => { it('render a token icon based on the provided field type', () => { expect( - shallow() + shallow() .find(EuiToken) .prop('iconType') ).toBe('tokenString'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.tsx index 773fcd19ce9ea..353d303da2b5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_token.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { EuiToken } from '@elastic/eui'; +import { SchemaType, InternalSchemaType } from '../../../shared/schema/types'; + import { FieldType } from './types'; interface Props { @@ -16,13 +18,14 @@ interface Props { } const fieldTypeToTokenMap = { - text: 'tokenString', - string: 'tokenString', - number: 'tokenNumber', - float: 'tokenNumber', - location: 'tokenGeo', - geolocation: 'tokenGeo', - date: 'tokenDate', + [SchemaType.Text]: 'tokenString', + [InternalSchemaType.String]: 'tokenString', + [SchemaType.Number]: 'tokenNumber', + [InternalSchemaType.Float]: 'tokenNumber', + [SchemaType.Geolocation]: 'tokenGeo', + [InternalSchemaType.Location]: 'tokenGeo', + [SchemaType.Date]: 'tokenDate', + [InternalSchemaType.Date]: 'tokenDate', }; export const ResultToken: React.FC = ({ fieldType }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts index 638a76511deee..4be3eb137177b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts @@ -7,9 +7,9 @@ import { EuiButtonIconColor } from '@elastic/eui'; -import { InternalSchemaTypes, SchemaTypes } from '../../../shared/types'; +import { InternalSchemaType, SchemaType } from '../../../shared/schema/types'; -export type FieldType = InternalSchemaTypes | SchemaTypes; +export type FieldType = InternalSchemaType | SchemaType; export type Raw = string | string[] | number | number[]; export type Snippet = string; 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 e432ba6956094..6522d84aef156 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 @@ -13,7 +13,7 @@ import { omit } from 'lodash'; import { nextTick } from '@kbn/test/jest'; -import { Schema, SchemaConflicts, SchemaTypes } from '../../../shared/types'; +import { Schema, SchemaConflicts, SchemaType } from '../../../shared/schema/types'; import { ServerFieldResultSettingObject } from './types'; @@ -77,9 +77,9 @@ describe('ResultSettingsLogic', () => { bar: { raw: { size: 5 } }, }; const schema: Schema = { - foo: 'text' as SchemaTypes, - bar: 'number' as SchemaTypes, - baz: 'text' as SchemaTypes, + foo: SchemaType.Text, + bar: SchemaType.Number, + baz: SchemaType.Text, }; const schemaConflicts: SchemaConflicts = { foo: { @@ -437,7 +437,7 @@ describe('ResultSettingsLogic', () => { it('considers a text value with raw set (but no size) as worth 1.5', () => { mount({ resultFields: { foo: { raw: true } }, - schema: { foo: 'text' as SchemaTypes }, + schema: { foo: SchemaType.Text }, }); expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); }); @@ -445,7 +445,7 @@ describe('ResultSettingsLogic', () => { it('considers a text value with raw set and a size over 250 as also worth 1.5', () => { mount({ resultFields: { foo: { raw: true, rawSize: 251 } }, - schema: { foo: 'text' as SchemaTypes }, + schema: { foo: SchemaType.Text }, }); expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); }); @@ -453,7 +453,7 @@ describe('ResultSettingsLogic', () => { it('considers a text value with raw set and a size less than or equal to 250 as worth 1', () => { mount({ resultFields: { foo: { raw: true, rawSize: 250 } }, - schema: { foo: 'text' as SchemaTypes }, + schema: { foo: SchemaType.Text }, }); expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1); }); @@ -461,7 +461,7 @@ describe('ResultSettingsLogic', () => { it('considers a text value with a snippet set as worth 2', () => { mount({ resultFields: { foo: { snippet: true, snippetSize: 50, snippetFallback: true } }, - schema: { foo: 'text' as SchemaTypes }, + schema: { foo: SchemaType.Text }, }); expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(2); }); @@ -469,7 +469,7 @@ describe('ResultSettingsLogic', () => { it('will sum raw and snippet values if both are set', () => { mount({ resultFields: { foo: { snippet: true, raw: true } }, - schema: { foo: 'text' as SchemaTypes }, + schema: { foo: SchemaType.Text }, }); // 1.5 (raw) + 2 (snippet) = 3.5 expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(3.5); @@ -478,7 +478,7 @@ describe('ResultSettingsLogic', () => { it('considers a non-text value with raw set as 0.2', () => { mount({ resultFields: { foo: { raw: true } }, - schema: { foo: 'number' as SchemaTypes }, + schema: { foo: SchemaType.Number }, }); expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(0.2); }); @@ -491,9 +491,9 @@ describe('ResultSettingsLogic', () => { baz: { raw: true }, }, schema: { - foo: 'text' as SchemaTypes, - bar: 'text' as SchemaTypes, - baz: 'number' as SchemaTypes, + foo: SchemaType.Text, + bar: SchemaType.Text, + baz: SchemaType.Number, }, }); // 1.5 (foo) + 3.5 (bar) + baz (.2) = 5.2 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 4e738961f5e58..e7ac94e9f9d2d 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 @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; -import { Schema, SchemaConflicts } from '../../../shared/types'; +import { Schema, SchemaConflicts } from '../../../shared/schema/types'; import { EngineLogic } from '../engine'; import { DEFAULT_SNIPPET_SIZE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts index 7e1d3d96c6d3f..a2dae8cbdcb4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SchemaTypes } from '../../../shared/types'; +import { SchemaType } from '../../../shared/schema/types'; import { areFieldsAtDefaultSettings, @@ -56,7 +56,7 @@ describe('convertServerResultFieldsToResultFields', () => { }, }, { - foo: 'text' as SchemaTypes, + foo: SchemaType.Text, } ) ).toEqual({ @@ -132,8 +132,8 @@ describe('splitResultFields', () => { }, }, { - foo: 'text' as SchemaTypes, - bar: 'number' as SchemaTypes, + foo: SchemaType.Text, + bar: SchemaType.Number, } ) ).toEqual({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts index a67f092a5e7f7..0146a1fe0ed51 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts @@ -7,7 +7,7 @@ import { isEqual } from 'lodash'; -import { Schema } from '../../../shared/types'; +import { Schema } from '../../../shared/schema/types'; import { DEFAULT_FIELD_SETTINGS, DISABLED_FIELD_SETTINGS } from './constants'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/index.ts index 0c286914d5c2b..c0e9ae19e075b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/index.ts @@ -6,3 +6,4 @@ */ export { SCHEMA_TITLE } from './constants'; +export { SchemaRouter } from './schema_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/index.ts new file mode 100644 index 0000000000000..5ed22298c4862 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ReindexJob } from './reindex_job'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx new file mode 100644 index 0000000000000..9e8386e2e8337 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../__mocks__/react_router_history.mock'; + +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + +import { ReindexJob } from './'; + +describe('ReindexJob', () => { + const props = { + schemaBreadcrumb: ['Engines', 'some-engine', 'Schema'], + }; + + beforeEach(() => { + (useParams as jest.Mock).mockReturnValueOnce({ reindexJobId: 'abc1234567890' }); + }); + + it('renders', () => { + shallow(); + // TODO: Check child components + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx new file mode 100644 index 0000000000000..19da08d446300 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx @@ -0,0 +1,45 @@ +/* + * 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 { useParams } from 'react-router-dom'; + +import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FlashMessages } from '../../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; +import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; + +interface Props { + schemaBreadcrumb: BreadcrumbTrail; +} + +export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => { + const { reindexJobId } = useParams() as { reindexJobId: string }; + + return ( + <> + + + + {reindexJobId} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.test.tsx new file mode 100644 index 0000000000000..13a94c666509b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, rerender } from '../../../__mocks__'; +import '../../__mocks__/engine_logic.mock'; + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + +import { ReindexJob } from './reindex_job'; +import { Schema, MetaEngineSchema } from './views'; + +import { SchemaRouter } from './'; + +describe('SchemaRouter', () => { + const wrapper = shallow(); + + it('renders', () => { + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + }); + + it('renders the ReindexJob route', () => { + expect(wrapper.find(ReindexJob)).toHaveLength(1); + }); + + it('renders the MetaEngineSchema view if the current engine is a meta engine', () => { + setMockValues({ isMetaEngine: true }); + rerender(wrapper); + + expect(wrapper.find(MetaEngineSchema)).toHaveLength(1); + expect(wrapper.find(Schema)).toHaveLength(0); + }); + + it('renders the default Schema view if the current engine is not a meta engine', () => { + setMockValues({ isMetaEngine: false }); + rerender(wrapper); + + expect(wrapper.find(Schema)).toHaveLength(1); + expect(wrapper.find(MetaEngineSchema)).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx new file mode 100644 index 0000000000000..bfa346fee468b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { useValues } from 'kea'; + +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { ENGINE_REINDEX_JOB_PATH } from '../../routes'; +import { EngineLogic, getEngineBreadcrumbs } from '../engine'; + +import { SCHEMA_TITLE } from './constants'; +import { ReindexJob } from './reindex_job'; +import { Schema, MetaEngineSchema } from './views'; + +export const SchemaRouter: React.FC = () => { + const { isMetaEngine } = useValues(EngineLogic); + const schemaBreadcrumb = getEngineBreadcrumbs([SCHEMA_TITLE]); + + return ( + + + + + + + {isMetaEngine ? : } + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/index.ts new file mode 100644 index 0000000000000..24f8edd856e48 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Schema } from './schema'; +export { MetaEngineSchema } from './meta_engine_schema'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx new file mode 100644 index 0000000000000..8412af6455285 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { MetaEngineSchema } from './'; + +describe('MetaEngineSchema', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(false); + // TODO: Check for schema components + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx new file mode 100644 index 0000000000000..d79ddae3d9b78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FlashMessages } from '../../../../shared/flash_messages'; + +export const MetaEngineSchema: React.FC = () => { + return ( + <> + + + TODO + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx new file mode 100644 index 0000000000000..5b6367d9ce668 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiPageHeader, EuiButton } from '@elastic/eui'; + +import { Schema } from './'; + +describe('Schema', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(false); + // TODO: Check for schema components + }); + + it('renders page action buttons', () => { + const wrapper = shallow() + .find(EuiPageHeader) + .dive() + .children() + .dive(); + + expect(wrapper.find(EuiButton)).toHaveLength(2); + // TODO: Expect click actions + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx new file mode 100644 index 0000000000000..ad53fd2c718b2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiPageHeader, EuiButton, EuiPageContentBody } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FlashMessages } from '../../../../shared/flash_messages'; + +export const Schema: React.FC = () => { + return ( + <> + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.updateSchemaButtonLabel', + { defaultMessage: 'Update types' } + )} + , + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.createSchemaFieldButtonLabel', + { defaultMessage: 'Create a schema field' } + )} + , + ]} + /> + + TODO + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/index.ts index f161f891eb4a3..5de1224a9f28a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/index.ts @@ -7,3 +7,4 @@ export { SEARCH_UI_TITLE } from './constants'; export { SearchUI } from './search_ui'; +export { SearchUILogic } from './search_ui_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx index 352ef257dc8a2..34c0669cc476e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx @@ -5,8 +5,11 @@ * 2.0. */ +import '../../../__mocks__/shallow_useeffect.mock'; import '../../__mocks__/engine_logic.mock'; +import { setMockActions } from '../../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; @@ -14,8 +17,22 @@ import { shallow } from 'enzyme'; import { SearchUI } from './'; describe('SearchUI', () => { + const actions = { + loadFieldData: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + it('renders', () => { shallow(); // TODO: Check for form }); + + it('initializes data on mount', () => { + shallow(); + expect(actions.loadFieldData).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx index 086769f1556e9..d4e4d72e4740a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useActions } from 'kea'; import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; @@ -15,8 +17,15 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro import { getEngineBreadcrumbs } from '../engine'; import { SEARCH_UI_TITLE } from './constants'; +import { SearchUILogic } from './search_ui_logic'; export const SearchUI: React.FC = () => { + const { loadFieldData } = useActions(SearchUILogic); + + useEffect(() => { + loadFieldData(); + }, []); + return ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts new file mode 100644 index 0000000000000..29261f3a4031f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts @@ -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 { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; + +import { mockEngineValues } from '../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; + +import { ActiveField } from './types'; + +import { SearchUILogic } from './'; + +describe('SearchUILogic', () => { + const { mount } = new LogicMounter(SearchUILogic); + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + const DEFAULT_VALUES = { + dataLoading: true, + validFields: [], + validSortFields: [], + validFacetFields: [], + titleField: '', + urlField: '', + facetFields: [], + sortFields: [], + activeField: ActiveField.None, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockEngineValues.engineName = 'engine1'; + }); + + it('has expected default values', () => { + mount(); + expect(SearchUILogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onFieldDataLoaded', () => { + it('sets initial field values fetched from API call and sets dataLoading to false', () => { + mount({ + validFields: [], + validSortFields: [], + validFacetFields: [], + }); + + SearchUILogic.actions.onFieldDataLoaded({ + validFields: ['foo'], + validSortFields: ['bar'], + validFacetFields: ['baz'], + }); + + expect(SearchUILogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + validFields: ['foo'], + validSortFields: ['bar'], + validFacetFields: ['baz'], + }); + }); + }); + + describe('onTitleFieldChange', () => { + it('sets the titleField value', () => { + mount({ titleField: '' }); + SearchUILogic.actions.onTitleFieldChange('foo'); + expect(SearchUILogic.values).toEqual({ + ...DEFAULT_VALUES, + titleField: 'foo', + }); + }); + }); + + describe('onURLFieldChange', () => { + it('sets the urlField value', () => { + mount({ urlField: '' }); + SearchUILogic.actions.onURLFieldChange('foo'); + expect(SearchUILogic.values).toEqual({ + ...DEFAULT_VALUES, + urlField: 'foo', + }); + }); + }); + + describe('onFacetFieldsChange', () => { + it('sets the facetFields value', () => { + mount({ facetFields: [] }); + SearchUILogic.actions.onFacetFieldsChange(['foo']); + expect(SearchUILogic.values).toEqual({ + ...DEFAULT_VALUES, + facetFields: ['foo'], + }); + }); + }); + + describe('onSortFieldsChange', () => { + it('sets the sortFields value', () => { + mount({ sortFields: [] }); + SearchUILogic.actions.onSortFieldsChange(['foo']); + expect(SearchUILogic.values).toEqual({ + ...DEFAULT_VALUES, + sortFields: ['foo'], + }); + }); + }); + + describe('onActiveFieldChange', () => { + it('sets the activeField value', () => { + mount({ activeField: '' }); + SearchUILogic.actions.onActiveFieldChange(ActiveField.Sort); + expect(SearchUILogic.values).toEqual({ + ...DEFAULT_VALUES, + activeField: ActiveField.Sort, + }); + }); + }); + }); + + describe('listeners', () => { + const MOCK_RESPONSE = { + validFields: ['test'], + validSortFields: ['test'], + validFacetFields: ['test'], + }; + + describe('loadFieldData', () => { + it('should make an API call and set state based on the response', async () => { + http.get.mockReturnValueOnce(Promise.resolve(MOCK_RESPONSE)); + mount(); + jest.spyOn(SearchUILogic.actions, 'onFieldDataLoaded'); + + SearchUILogic.actions.loadFieldData(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/api/app_search/engines/engine1/search_ui/field_config' + ); + expect(SearchUILogic.actions.onFieldDataLoaded).toHaveBeenCalledWith(MOCK_RESPONSE); + }); + + it('handles errors', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + mount(); + + SearchUILogic.actions.loadFieldData(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.ts new file mode 100644 index 0000000000000..7b3454c9e8413 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; + +import { ActiveField } from './types'; + +interface InitialFieldValues { + validFields: string[]; + validSortFields: string[]; + validFacetFields: string[]; +} +interface SearchUIActions { + loadFieldData(): void; + onFieldDataLoaded(initialFieldValues: InitialFieldValues): InitialFieldValues; + onActiveFieldChange(activeField: ActiveField): { activeField: ActiveField }; + onFacetFieldsChange(facetFields: string[]): { facetFields: string[] }; + onSortFieldsChange(sortFields: string[]): { sortFields: string[] }; + onTitleFieldChange(titleField: string): { titleField: string }; + onURLFieldChange(urlField: string): { urlField: string }; +} + +interface SearchUIValues { + dataLoading: boolean; + validFields: string[]; + validSortFields: string[]; + validFacetFields: string[]; + titleField: string; + urlField: string; + facetFields: string[]; + sortFields: string[]; + activeField: ActiveField; +} + +export const SearchUILogic = kea>({ + path: ['enterprise_search', 'app_search', 'search_ui_logic'], + actions: () => ({ + loadFieldData: () => true, + onFieldDataLoaded: (initialFieldValues) => initialFieldValues, + onActiveFieldChange: (activeField) => ({ activeField }), + onFacetFieldsChange: (facetFields) => ({ facetFields }), + onSortFieldsChange: (sortFields) => ({ sortFields }), + onTitleFieldChange: (titleField) => ({ titleField }), + onURLFieldChange: (urlField) => ({ urlField }), + }), + reducers: () => ({ + dataLoading: [ + true, + { + onFieldDataLoaded: () => false, + }, + ], + validFields: [[], { onFieldDataLoaded: (_, { validFields }) => validFields }], + validSortFields: [[], { onFieldDataLoaded: (_, { validSortFields }) => validSortFields }], + validFacetFields: [[], { onFieldDataLoaded: (_, { validFacetFields }) => validFacetFields }], + titleField: ['', { onTitleFieldChange: (_, { titleField }) => titleField }], + urlField: ['', { onURLFieldChange: (_, { urlField }) => urlField }], + facetFields: [[], { onFacetFieldsChange: (_, { facetFields }) => facetFields }], + sortFields: [[], { onSortFieldsChange: (_, { sortFields }) => sortFields }], + activeField: [ActiveField.None, { onActiveFieldChange: (_, { activeField }) => activeField }], + }), + listeners: ({ actions }) => ({ + loadFieldData: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/search_ui/field_config`; + + try { + const initialFieldValues = await http.get(url); + + actions.onFieldDataLoaded(initialFieldValues); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/types.ts new file mode 100644 index 0000000000000..132ce46bc13fb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum ActiveField { + Title, + Filter, + Sort, + Url, + None, +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 727312801c610..872db3e149b60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -35,8 +35,8 @@ export const ENGINE_ANALYTICS_QUERY_DETAIL_PATH = `${ENGINE_ANALYTICS_QUERY_DETA export const ENGINE_DOCUMENTS_PATH = `${ENGINE_PATH}/documents`; export const ENGINE_DOCUMENT_DETAIL_PATH = `${ENGINE_DOCUMENTS_PATH}/:documentId`; -export const ENGINE_SCHEMA_PATH = `${ENGINE_PATH}/schema/edit`; -export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_PATH}/reindex-job/:activeReindexJobId`; +export const ENGINE_SCHEMA_PATH = `${ENGINE_PATH}/schema`; +export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_SCHEMA_PATH}/reindex_job/:reindexJobId`; export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`; // TODO: Crawler sub-pages diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/field_types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/field_types.ts deleted file mode 100644 index 6db76e14f5d3b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/field_types.ts +++ /dev/null @@ -1,18 +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. - */ - -export const TEXT = 'text'; -export const NUMBER = 'number'; -export const DATE = 'date'; -export const GEOLOCATION = 'geolocation'; - -export const fieldTypeSelectOptions = [ - { value: TEXT, text: TEXT }, - { value: NUMBER, text: NUMBER }, - { value: DATE, text: DATE }, - { value: GEOLOCATION, text: GEOLOCATION }, -]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx index bd3eacacb04e5..172e74a1454ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -11,7 +11,7 @@ import { useValues, useActions } from 'kea'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; -import { IIndexingStatus } from '../types'; +import { IIndexingStatus } from '../schema/types'; import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts index a5f3f7ad3d067..0195f6a388870 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts @@ -9,7 +9,7 @@ import { kea, MakeLogicType } from 'kea'; import { flashAPIErrors } from '../flash_messages'; import { HttpLogic } from '../http'; -import { IIndexingStatus } from '../types'; +import { IIndexingStatus } from '../schema/types'; interface IndexingStatusProps { statusPath: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/schema/constants.ts index 711444d0efb46..3791626f54398 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/constants.ts @@ -69,10 +69,3 @@ export const ERROR_TABLE_VIEW_LINK = i18n.translate( defaultMessage: 'View', } ); - -export const RECENTY_ADDED = i18n.translate( - 'xpack.enterpriseSearch.schema.existingField.status.recentlyAdded', - { - defaultMessage: 'Recently Added', - } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx similarity index 50% rename from x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx index 5e89dce24bd4a..df28719839011 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx @@ -11,9 +11,9 @@ import { shallow } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; -import { SchemaExistingField } from './'; +import { SchemaFieldTypeSelect } from './'; -describe('SchemaExistingField', () => { +describe('SchemaFieldTypeSelect', () => { const updateExistingFieldType = jest.fn(); const props = { fieldName: 'foo', @@ -22,33 +22,21 @@ describe('SchemaExistingField', () => { }; it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiSelect)).toHaveLength(1); }); - it('renders no EuiSelect without props', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiSelect)).toHaveLength(0); - }); - it('calls updateExistingFieldType when the select value is changed', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find(EuiSelect).simulate('change', { target: { value: 'bar' } }); expect(updateExistingFieldType).toHaveBeenCalledWith(props.fieldName, 'bar'); }); - it('doesn`t render fieldName when hidden', () => { - const wrapper = shallow(); - - expect(wrapper.find('.c-stui-engine-schema-field__name').contains(props.fieldName)).toBeFalsy(); - }); - - it('renders unconfirmed message', () => { - const wrapper = shallow(); + it('passes disabled state', () => { + const wrapper = shallow(); - expect(wrapper.find('.c-stui-engine-schema-field__status').exists()).toBeTruthy(); + expect(wrapper.find(EuiSelect).prop('disabled')).toEqual(true); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx new file mode 100644 index 0000000000000..8dfd87f4015d6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiSelect } from '@elastic/eui'; + +import { SchemaType } from '../types'; + +interface Props { + fieldName: string; + fieldType: string; + updateExistingFieldType(fieldName: string, fieldType: string): void; + disabled?: boolean; +} + +export const SchemaFieldTypeSelect: React.FC = ({ + fieldName, + fieldType, + updateExistingFieldType, + disabled, +}) => { + const fieldTypeOptions = Object.values(SchemaType).map((type) => ({ value: type, text: type })); + return ( + updateExistingFieldType(fieldName, e.target.value)} + data-test-subj="SchemaSelect" + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/schema/index.ts index dcf5a4cc7faf2..04bef009587c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/index.ts @@ -6,4 +6,4 @@ */ export { SchemaAddFieldModal } from './schema_add_field_modal'; -export { SchemaExistingField } from './schema_existing_field'; +export { SchemaFieldTypeSelect } from './field_type_select'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx index 88c170b059d9c..12bc61d723919 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx @@ -9,13 +9,12 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; -import { EuiFieldText, EuiModal, EuiSelect } from '@elastic/eui'; - -import { NUMBER } from '../constants/field_types'; +import { EuiFieldText, EuiModal } from '@elastic/eui'; import { FIELD_NAME_CORRECTED_PREFIX } from './constants'; +import { SchemaType } from './types'; -import { SchemaAddFieldModal } from './'; +import { SchemaFieldTypeSelect, SchemaAddFieldModal } from './'; describe('SchemaAddFieldModal', () => { const addNewField = jest.fn(); @@ -78,11 +77,13 @@ describe('SchemaAddFieldModal', () => { ); }); - it('handles option change', () => { + it('handles field type select change', () => { const wrapper = shallow(); - wrapper.find(EuiSelect).simulate('change', { target: { value: NUMBER } }); + const fieldTypeUpdate = wrapper.find(SchemaFieldTypeSelect).prop('updateExistingFieldType'); + + fieldTypeUpdate('_', SchemaType.Number); // The fieldName arg is irrelevant for this modal - expect(wrapper.find('[data-test-subj="SchemaSelect"]').prop('value')).toEqual(NUMBER); + expect(wrapper.find(SchemaFieldTypeSelect).prop('fieldType')).toEqual(SchemaType.Number); }); it('handles form submission', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx index ce3df17bf7a4f..e6f7bffc2d83f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx @@ -20,12 +20,10 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiSelect, EuiSpacer, } from '@elastic/eui'; import { CANCEL_BUTTON_LABEL } from '../constants'; -import { TEXT, fieldTypeSelectOptions } from '../constants/field_types'; import { FIELD_NAME_CORRECT_NOTE, @@ -34,6 +32,9 @@ import { FIELD_NAME_MODAL_DESCRIPTION, FIELD_NAME_MODAL_ADD_FIELD, } from './constants'; +import { SchemaType } from './types'; + +import { SchemaFieldTypeSelect } from './'; interface ISchemaAddFieldModalProps { disableForm?: boolean; @@ -49,7 +50,7 @@ export const SchemaAddFieldModal: React.FC = ({ disableForm, }) => { const [loading, setLoading] = useState(false); - const [newFieldType, updateNewFieldType] = useState(TEXT); + const [newFieldType, updateNewFieldType] = useState(SchemaType.Text); const [formattedFieldName, setFormattedFieldName] = useState(''); const [rawFieldName, setRawFieldName] = useState(''); @@ -112,13 +113,11 @@ export const SchemaAddFieldModal: React.FC = ({ - updateNewFieldType(type)} disabled={disableForm} - onChange={(e) => updateNewFieldType(e.target.value)} data-test-subj="SchemaSelect" /> diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx index 09f499e540e93..c2b3ab614fe47 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx @@ -32,18 +32,10 @@ import { ERROR_TABLE_REVIEW_CONTROL, ERROR_TABLE_VIEW_LINK, } from './constants'; - -interface IFieldCoercionError { - external_id: string; - error: string; -} - -interface IFieldCoercionErrors { - [key: string]: IFieldCoercionError[]; -} +import { FieldCoercionErrors } from './types'; interface ISchemaErrorsAccordionProps { - fieldCoercionErrors: IFieldCoercionErrors; + fieldCoercionErrors: FieldCoercionErrors; schema: { [key: string]: string }; itemId?: string; getRoute?(itemId: string, externalId: string): string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.tsx deleted file mode 100644 index 58408f85d8c40..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.tsx +++ /dev/null @@ -1,58 +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 classNames from 'classnames'; - -import { EuiSelect } from '@elastic/eui'; - -import { fieldTypeSelectOptions } from '../constants/field_types'; - -import { RECENTY_ADDED } from './constants'; - -interface ISchemaExistingFieldProps { - disabled?: boolean; - fieldName: string; - fieldType?: string; - unconfirmed?: boolean; - hideName?: boolean; - updateExistingFieldType?(fieldName: string, fieldType: string): void; -} - -export const SchemaExistingField: React.FC = ({ - disabled, - fieldName, - fieldType, - unconfirmed, - hideName, - updateExistingFieldType, -}) => { - const fieldCssClass = classNames('c-stui-engine-schema-field', { - 'c-stui-engine-schema-field--recently-added': unconfirmed, - }); - - return ( -
-
{!hideName ? fieldName : ''}
- {unconfirmed &&
{RECENTY_ADDED}
} - {fieldType && updateExistingFieldType && ( -
- updateExistingFieldType(fieldName, e.target.value)} - data-test-subj="SchemaSelect" - /> -
- )} -
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/schema/types.ts new file mode 100644 index 0000000000000..916478a0d9ccf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/types.ts @@ -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. + */ + +/** + * Schema types + */ + +export enum SchemaType { + Text = 'text', + Number = 'number', + Geolocation = 'geolocation', + Date = 'date', +} +// Certain API endpoints will use these internal type names, which map to the external names above +export enum InternalSchemaType { + String = 'string', + Float = 'float', + Location = 'location', + Date = 'date', +} + +export type Schema = Record; + +/** + * Schema conflict types + */ + +// This is a mapping of schema field types ("text", "number", "geolocation", "date") +// to the names of source engines which utilize that type +export type SchemaConflictFieldTypes = Record; + +export interface SchemaConflict { + fieldTypes: SchemaConflictFieldTypes; + resolution?: string; +} + +// For now these values are SchemaConflictFieldTypes, but in the near future will be SchemaConflict +// once we implement schema conflict resolution +export type SchemaConflicts = Record; + +/** + * Indexing job / errors types + */ + +export interface IIndexingStatus { + percentageComplete: number; + numDocumentsWithErrors: number; + activeReindexJobId: string; +} + +export interface IndexJob extends IIndexingStatus { + isActive?: boolean; + hasErrors?: boolean; +} + +export interface FieldCoercionError { + external_id: string; + error: string; +} +export type FieldCoercionErrors = Record; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index e026e2f592e75..e807af6abaf50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -7,41 +7,6 @@ import { ADD, UPDATE } from './constants/operations'; -export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; -// Certain API endpoints will use these internal type names, which map to the external names above -export type InternalSchemaTypes = 'string' | 'float' | 'location' | 'date'; -export interface Schema { - [key: string]: SchemaTypes; -} - -// this is a mapping of schema field types ("string", "number", "geolocation", "date") to the names -// of source engines which utilize that type -export type SchemaConflictFieldTypes = { - [key in SchemaTypes]: string[]; -}; - -export interface SchemaConflict { - fieldTypes: SchemaConflictFieldTypes; - resolution?: string; -} - -// For now these values are ISchemaConflictFieldTypes, but in the near future will be ISchemaConflict -// once we implement schema conflict resolution -export interface SchemaConflicts { - [key: string]: SchemaConflictFieldTypes; -} - -export interface IIndexingStatus { - percentageComplete: number; - numDocumentsWithErrors: number; - activeReindexJobId: string; -} - -export interface IndexJob extends IIndexingStatus { - isActive?: boolean; - hasErrors?: boolean; -} - export type TOperation = typeof ADD | typeof UPDATE; export interface RoleRules { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx index bc0363d55da69..9996e58e819b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; +import { SchemaFieldTypeSelect } from '../../../../../shared/schema'; import { SchemaFieldsTable } from './schema_fields_table'; @@ -31,7 +31,7 @@ describe('SchemaFieldsTable', () => { setMockValues({ filterValue, filteredSchemaFields }); const wrapper = shallow(); - expect(wrapper.find(SchemaExistingField)).toHaveLength(1); + expect(wrapper.find(SchemaFieldTypeSelect)).toHaveLength(1); }); it('handles no results', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx index 3f56a2cfc745b..db8a816f81cb4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; +import { SchemaFieldTypeSelect } from '../../../../../shared/schema'; import { SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER, @@ -53,11 +53,9 @@ export const SchemaFieldsTable: React.FC = () => {
- diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index 74e3337e9600a..650909c0b5a82 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -22,8 +22,8 @@ jest.mock('../../../../app_logic', () => ({ const spyScrollTo = jest.fn(); Object.defineProperty(global.window, 'scrollTo', { value: spyScrollTo }); -import { TEXT } from '../../../../../shared/constants/field_types'; import { ADD, UPDATE } from '../../../../../shared/constants/operations'; +import { SchemaType } from '../../../../../shared/schema/types'; import { AppLogic } from '../../../../app_logic'; import { @@ -54,7 +54,7 @@ describe('SchemaLogic', () => { addFieldFormErrors: null, mostRecentIndexJob: {}, fieldCoercionErrors: {}, - newFieldType: TEXT, + newFieldType: SchemaType.Text, rawFieldName: '', formUnchanged: true, dataLoading: true, @@ -113,7 +113,7 @@ describe('SchemaLogic', () => { expect(SchemaLogic.values.activeSchema).toEqual(schema); expect(SchemaLogic.values.serverSchema).toEqual(schema); expect(SchemaLogic.values.mostRecentIndexJob).toEqual(mostRecentIndexJob); - expect(SchemaLogic.values.newFieldType).toEqual(TEXT); + expect(SchemaLogic.values.newFieldType).toEqual(SchemaType.Text); expect(SchemaLogic.values.addFieldFormErrors).toEqual(null); expect(SchemaLogic.values.formUnchanged).toEqual(true); expect(SchemaLogic.values.showAddFieldModal).toEqual(false); @@ -128,10 +128,9 @@ describe('SchemaLogic', () => { }); it('updateNewFieldType', () => { - const NUMBER = 'number'; - SchemaLogic.actions.updateNewFieldType(NUMBER); + SchemaLogic.actions.updateNewFieldType(SchemaType.Number); - expect(SchemaLogic.values.newFieldType).toEqual(NUMBER); + expect(SchemaLogic.values.newFieldType).toEqual(SchemaType.Number); }); it('onFieldUpdate', () => { @@ -313,16 +312,16 @@ describe('SchemaLogic', () => { SchemaLogic.actions.onInitializeSchema(serverResponse); const newSchema = { ...schema, - bar: 'number', + bar: SchemaType.Number, }; - SchemaLogic.actions.addNewField('bar', 'number'); + SchemaLogic.actions.addNewField('bar', SchemaType.Number); expect(setServerFieldSpy).toHaveBeenCalledWith(newSchema, ADD); }); it('handles duplicate', () => { SchemaLogic.actions.onInitializeSchema(serverResponse); - SchemaLogic.actions.addNewField('foo', 'number'); + SchemaLogic.actions.addNewField('foo', SchemaType.Number); expect(setErrorMessage).toHaveBeenCalledWith('New field already exists: foo.'); }); @@ -332,9 +331,9 @@ describe('SchemaLogic', () => { const onFieldUpdateSpy = jest.spyOn(SchemaLogic.actions, 'onFieldUpdate'); SchemaLogic.actions.onInitializeSchema(serverResponse); const newSchema = { - foo: 'number', + foo: SchemaType.Number, }; - SchemaLogic.actions.updateExistingFieldType('foo', 'number'); + SchemaLogic.actions.updateExistingFieldType('foo', SchemaType.Number); expect(onFieldUpdateSpy).toHaveBeenCalledWith({ schema: newSchema, formUnchanged: false }); }); @@ -455,7 +454,7 @@ describe('SchemaLogic', () => { it('handles filtered response', () => { const newSchema = { ...schema, - bar: 'number', + bar: SchemaType.Number, }; SchemaLogic.actions.onInitializeSchema(serverResponse); SchemaLogic.actions.onFieldUpdate({ schema: newSchema, formUnchanged: false }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts index c97c6f5f0c1be..b2c329f0544fd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -10,7 +10,6 @@ import { cloneDeep, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { TEXT } from '../../../../../shared/constants/field_types'; import { ADD, UPDATE } from '../../../../../shared/constants/operations'; import { flashAPIErrors, @@ -19,7 +18,13 @@ import { clearFlashMessages, } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; -import { IndexJob, TOperation, Schema, SchemaTypes } from '../../../../../shared/types'; +import { + IndexJob, + FieldCoercionErrors, + Schema, + SchemaType, +} from '../../../../../shared/schema/types'; +import { TOperation } from '../../../../../shared/types'; import { AppLogic } from '../../../../app_logic'; import { OptionValue } from '../../../../types'; import { SourceLogic } from '../../source_logic'; @@ -37,7 +42,7 @@ interface SchemaActions { ): SchemaChangeErrorsProps; onSchemaSetSuccess(schemaProps: SchemaResponseProps): SchemaResponseProps; onSchemaSetFormErrors(errors: string[]): string[]; - updateNewFieldType(newFieldType: SchemaTypes): SchemaTypes; + updateNewFieldType(newFieldType: SchemaType): SchemaType; onFieldUpdate({ schema, formUnchanged, @@ -51,8 +56,8 @@ interface SchemaActions { setFilterValue(filterValue: string): string; addNewField( fieldName: string, - newFieldType: SchemaTypes - ): { fieldName: string; newFieldType: SchemaTypes }; + newFieldType: SchemaType + ): { fieldName: string; newFieldType: SchemaType }; updateFields(): void; openAddFieldModal(): void; closeAddFieldModal(): void; @@ -64,8 +69,8 @@ interface SchemaActions { ): { activeReindexJobId: string; sourceId: string }; updateExistingFieldType( fieldName: string, - newFieldType: SchemaTypes - ): { fieldName: string; newFieldType: SchemaTypes }; + newFieldType: SchemaType + ): { fieldName: string; newFieldType: SchemaType }; setServerField( updatedSchema: Schema, operation: TOperation @@ -98,15 +103,6 @@ export interface SchemaInitialData extends SchemaResponseProps { sourceId: string; } -interface FieldCoercionError { - external_id: string; - error: string; -} - -export interface FieldCoercionErrors { - [key: string]: FieldCoercionError[]; -} - interface SchemaChangeErrorsProps { fieldCoercionErrors: FieldCoercionErrors; } @@ -142,7 +138,7 @@ export const SchemaLogic = kea>({ activeReindexJobId, sourceId, }), - addNewField: (fieldName: string, newFieldType: SchemaTypes) => ({ fieldName, newFieldType }), + addNewField: (fieldName: string, newFieldType: SchemaType) => ({ fieldName, newFieldType }), updateExistingFieldType: (fieldName: string, newFieldType: string) => ({ fieldName, newFieldType, @@ -196,10 +192,10 @@ export const SchemaLogic = kea>({ }, ], newFieldType: [ - TEXT, + SchemaType.Text, { updateNewFieldType: (_, newFieldType) => newFieldType, - onSchemaSetSuccess: () => TEXT, + onSchemaSetSuccess: () => SchemaType.Text, }, ], addFieldFormErrors: [ diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index abe5272fe3263..3223471e4fc1a 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -100,6 +100,17 @@ describe('EnterpriseSearchRequestHandler', () => { }); }); + it('passes a body if that body is a string buffer', async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/example', + }); + await makeAPICall(requestHandler, { body: Buffer.from('{"bodacious":true}') }); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example', { + body: '{"bodacious":true}', + }); + }); + it('passes request params', async () => { const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/example', diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 6b6886cbbb75d..6ecdb8d8857c6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -17,6 +17,7 @@ import { registerOnboardingRoutes } from './onboarding'; import { registerResultSettingsRoutes } from './result_settings'; import { registerRoleMappingsRoutes } from './role_mappings'; import { registerSearchSettingsRoutes } from './search_settings'; +import { registerSearchUIRoutes } from './search_ui'; import { registerSettingsRoutes } from './settings'; import { registerSynonymsRoutes } from './synonyms'; @@ -31,6 +32,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerSynonymsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); registerRoleMappingsRoutes(dependencies); + registerSearchUIRoutes(dependencies); registerResultSettingsRoutes(dependencies); registerApiLogsRoutes(dependencies); registerOnboardingRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_ui.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_ui.test.ts new file mode 100644 index 0000000000000..8ddb254a3cde1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_ui.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; + +import { registerSearchUIRoutes } from './search_ui'; + +describe('reference application routes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('GET /api/app_search/engines/{engineName}/search_settings/details', () => { + const mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/search_ui/field_config', + }); + + beforeEach(() => { + registerSearchUIRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/search_ui/field_config', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_ui.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_ui.ts new file mode 100644 index 0000000000000..160b1454c5f22 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_ui.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerSearchUIRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{engineName}/search_ui/field_config', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/search_ui/field_config', + }) + ); +} 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 a1ac30995f722..5e8dfbea64f01 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 @@ -284,7 +284,11 @@ export const AgentPolicyForm: React.FunctionComponent = ({ }} /> - {isEditing && 'id' in agentPolicy && agentPolicy.is_managed !== true ? ( + {isEditing && + 'id' in agentPolicy && + !agentPolicy.is_managed && + !agentPolicy.is_default && + !agentPolicy.is_default_fleet_server ? ( diff --git a/x-pack/plugins/fleet/server/services/settings.test.ts b/x-pack/plugins/fleet/server/services/settings.test.ts index 87b3e163c1bb3..bec0f737c0bc8 100644 --- a/x-pack/plugins/fleet/server/services/settings.test.ts +++ b/x-pack/plugins/fleet/server/services/settings.test.ts @@ -5,14 +5,19 @@ * 2.0. */ +import { savedObjectsClientMock } from 'src/core/server/mocks'; + import { appContextService } from './app_context'; -import { getCloudFleetServersHosts } from './settings'; +import { getCloudFleetServersHosts, normalizeFleetServerHost, settingsSetup } from './settings'; jest.mock('./app_context'); const mockedAppContextService = appContextService as jest.Mocked; describe('getCloudFleetServersHosts', () => { + afterEach(() => { + mockedAppContextService.getCloud.mockReset(); + }); it('should return undefined if cloud is not setup', () => { expect(getCloudFleetServersHosts()).toBeUndefined(); }); @@ -49,3 +54,173 @@ describe('getCloudFleetServersHosts', () => { `); }); }); + +describe('settingsSetup', () => { + afterEach(() => { + mockedAppContextService.getCloud.mockReset(); + }); + it('should create settings if there is no settings', async () => { + const soClientMock = savedObjectsClientMock.create(); + + soClientMock.find.mockResolvedValue({ + total: 0, + page: 0, + per_page: 10, + saved_objects: [], + }); + + soClientMock.create.mockResolvedValue({ + id: 'created', + attributes: {}, + references: [], + type: 'so_type', + }); + + await settingsSetup(soClientMock); + + expect(soClientMock.create).toBeCalled(); + }); + + it('should do nothing if there is settings and no default fleet server hosts', async () => { + const soClientMock = savedObjectsClientMock.create(); + + soClientMock.find.mockResolvedValue({ + total: 1, + page: 0, + per_page: 10, + saved_objects: [ + { + id: 'defaultsettings', + attributes: {}, + type: 'so_type', + references: [], + score: 0, + }, + ], + }); + + soClientMock.create.mockResolvedValue({ + id: 'created', + attributes: {}, + references: [], + type: 'so_type', + }); + + await settingsSetup(soClientMock); + + expect(soClientMock.create).not.toBeCalled(); + }); + + it('should update settings if there is settings without fleet server hosts and default fleet server hosts', async () => { + const soClientMock = savedObjectsClientMock.create(); + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'test:dGVzdC5mcjo5MjQzJGRhM2I2YjNkYWY5ZDRjODE4ZjI4ZmEzNDdjMzgzODViJDgxMmY4NWMxZjNjZTQ2YTliYjgxZjFjMWIxMzRjNmRl', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + apm: {}, + }); + + soClientMock.find.mockResolvedValue({ + total: 1, + page: 0, + per_page: 10, + saved_objects: [ + { + id: 'defaultsettings', + attributes: {}, + type: 'so_type', + references: [], + score: 0, + }, + ], + }); + + soClientMock.update.mockResolvedValue({ + id: 'updated', + attributes: {}, + references: [], + type: 'so_type', + }); + + soClientMock.create.mockResolvedValue({ + id: 'created', + attributes: {}, + references: [], + type: 'so_type', + }); + + await settingsSetup(soClientMock); + + expect(soClientMock.create).not.toBeCalled(); + expect(soClientMock.update).toBeCalledWith('ingest_manager_settings', 'defaultsettings', { + fleet_server_hosts: ['https://deployment-id-1.fleet.test.fr:9243'], + }); + }); + + it('should not update settings if there is settings with fleet server hosts and default fleet server hosts', async () => { + const soClientMock = savedObjectsClientMock.create(); + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'test:dGVzdC5mcjo5MjQzJGRhM2I2YjNkYWY5ZDRjODE4ZjI4ZmEzNDdjMzgzODViJDgxMmY4NWMxZjNjZTQ2YTliYjgxZjFjMWIxMzRjNmRl', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + apm: {}, + }); + + soClientMock.find.mockResolvedValue({ + total: 1, + page: 0, + per_page: 10, + saved_objects: [ + { + id: 'defaultsettings', + attributes: { + fleet_server_hosts: ['http://fleetserver:1234'], + }, + type: 'so_type', + references: [], + score: 0, + }, + ], + }); + + soClientMock.update.mockResolvedValue({ + id: 'updated', + attributes: {}, + references: [], + type: 'so_type', + }); + + soClientMock.create.mockResolvedValue({ + id: 'created', + attributes: {}, + references: [], + type: 'so_type', + }); + + await settingsSetup(soClientMock); + + expect(soClientMock.create).not.toBeCalled(); + expect(soClientMock.update).not.toBeCalled(); + }); +}); + +describe('normalizeFleetServerHost', () => { + const scenarios = [ + { sourceUrl: 'http://test.fr', expectedUrl: 'http://test.fr:80' }, + { sourceUrl: 'http://test.fr/test/toto', expectedUrl: 'http://test.fr:80/test/toto' }, + { sourceUrl: 'https://test.fr', expectedUrl: 'https://test.fr:443' }, + { sourceUrl: 'https://test.fr/test/toto', expectedUrl: 'https://test.fr:443/test/toto' }, + { sourceUrl: 'https://test.fr:9243', expectedUrl: 'https://test.fr:9243' }, + { sourceUrl: 'https://test.fr:9243/test/toto', expectedUrl: 'https://test.fr:9243/test/toto' }, + ]; + + for (const scenario of scenarios) { + it(`should transform ${scenario.sourceUrl} correctly`, () => { + const url = normalizeFleetServerHost(scenario.sourceUrl); + + expect(url).toEqual(scenario.expectedUrl); + }); + } +}); diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 2046e2571c926..6b1a17fc5b060 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -29,6 +29,52 @@ export async function getSettings(soClient: SavedObjectsClientContract): Promise }; } +export async function settingsSetup(soClient: SavedObjectsClientContract) { + try { + const settings = await getSettings(soClient); + // Migration for < 7.13 Kibana + if (!settings.fleet_server_hosts || settings.fleet_server_hosts.length === 0) { + const defaultSettings = createDefaultSettings(); + if (defaultSettings.fleet_server_hosts.length > 0) { + return saveSettings(soClient, { + fleet_server_hosts: defaultSettings.fleet_server_hosts, + }); + } + } + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + const defaultSettings = createDefaultSettings(); + return saveSettings(soClient, defaultSettings); + } + + throw e; + } +} + +function getPortForURL(url: URL) { + if (url.port !== '') { + return url.port; + } + + if (url.protocol === 'http:') { + return '80'; + } + + if (url.protocol === 'https:') { + return '443'; + } +} + +export function normalizeFleetServerHost(host: string) { + // Fleet server is not using default port for http|https https://github.com/elastic/beats/issues/25420 + const fleetServerURL = new URL(host); + + // We are building the URL manualy as url format will not include the port if the port is 80 or 443 + return `${fleetServerURL.protocol}//${fleetServerURL.hostname}:${getPortForURL(fleetServerURL)}${ + fleetServerURL.pathname === '/' ? '' : fleetServerURL.pathname + }`; +} + export async function saveSettings( soClient: SavedObjectsClientContract, newData: Partial> @@ -36,10 +82,15 @@ export async function saveSettings( try { const settings = await getSettings(soClient); + const data = { ...newData }; + if (data.fleet_server_hosts) { + data.fleet_server_hosts = data.fleet_server_hosts.map(normalizeFleetServerHost); + } + const res = await soClient.update( GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, settings.id, - newData + data ); return { diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 0723186569df8..28deec8a89028 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -18,7 +18,6 @@ import { outputService } from './output'; import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_keys'; import { settingsService } from '.'; import { awaitIfPending } from './setup_utils'; -import { createDefaultSettings } from './settings'; import { ensureAgentActionPolicyChangeExists } from './agents'; import { awaitIfFleetServerSetupPending } from './fleet_server'; @@ -40,14 +39,7 @@ async function createSetupSideEffects( ): Promise { const [defaultOutput] = await Promise.all([ outputService.ensureDefaultOutput(soClient), - settingsService.getSettings(soClient).catch((e: any) => { - if (e.isBoom && e.output.statusCode === 404) { - const defaultSettings = createDefaultSettings(); - return settingsService.saveSettings(soClient, defaultSettings); - } - - return Promise.reject(e); - }), + settingsService.settingsSetup(soClient), ]); await awaitIfFleetServerSetupPending(); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index dc4a8dca60694..3c30c6d3a678f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -167,7 +167,7 @@ export const HotPhase: FunctionComponent = () => { <> {} - {license.canUseSearchableSnapshot() && } + {license.canUseSearchableSnapshot() && } )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 50663d936617b..7f99b10c776f7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -25,31 +25,60 @@ export interface Props { canBeDisabled?: boolean; } -const geti18nTexts = (phase: Props['phase']) => ({ - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { - defaultMessage: 'Searchable snapshot', - }), - description: - phase === 'frozen' ? ( - - ), - }} - /> - ) : ( - , - }} - /> - ), -}); +const geti18nTexts = (phase: Props['phase']) => { + switch (phase) { + // Hot and cold phases both create fully mounted snapshots. + case 'hot': + case 'cold': + return { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.fullyMountedSearchableSnapshotField.title', + { + defaultMessage: 'Searchable snapshot', + } + ), + description: ( + , + }} + /> + ), + toggleLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.fullyMountedSearchableSnapshotField.toggleLabel', + { defaultMessage: 'Convert to fully-mounted index' } + ), + }; + + // Frozen phase creates a partially mounted snapshot. + case 'frozen': + return { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.partiallyMountedSearchableSnapshotField.title', + { + defaultMessage: 'Searchable snapshot', + } + ), + description: ( + + ), + }} + /> + ), + toggleLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.partiallyMountedSearchableSnapshotField.toggleLabel', + { defaultMessage: 'Convert to partially-mounted index' } + ), + }; + } +}; export const SearchableSnapshotField: FunctionComponent = ({ phase, @@ -228,7 +257,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody', { defaultMessage: - 'Force merge, shrink, read only and freeze actions are not allowed when searchable snapshots are enabled in this phase.', + 'Force merge, shrink, read only, and freeze actions are not allowed when converting data to a fully-mounted index in this phase.', } )} data-test-subj="searchableSnapshotFieldsDisabledCallout" @@ -273,10 +302,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ disabled: isDisabledDueToLicense, onChange: setIsFieldToggleChecked, 'data-test-subj': 'searchableSnapshotToggle', - label: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel', - { defaultMessage: 'Create searchable snapshot' } - ), + label: i18nTexts.toggleLabel, } : undefined } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 2ad754ea61c89..bfc31c220825a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -80,7 +80,7 @@ export const i18nTexts = { searchableSnapshotsRepoFieldLabel: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel', { - defaultMessage: 'Searchable snapshot repository', + defaultMessage: 'Snapshot repository', } ), searchableSnapshotsStorageFieldLabel: i18n.translate( @@ -216,21 +216,21 @@ export const i18nTexts = { descriptions: { hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescription', { defaultMessage: - 'Store your most-recent, most frequently-searched data in the hot tier, which provides the best indexing and search performance at the highest cost.', + 'Store your most recent, most frequently-searched data in the hot tier. The hot tier provides the best indexing and search performance by using the most powerful, expensive hardware.', }), warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescription', { defaultMessage: - 'Move data to the warm tier, which is optimized for search performance over indexing performance. Data is infrequently added or updated in the warm phase.', + 'Move data to the warm tier when you are still likely to search it, but infrequently need to update it. The warm tier is optimized for search performance over indexing performance.', }), cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription', { defaultMessage: - 'Move data to the cold tier, which is optimized for cost savings over search performance. Data is normally read-only in the cold phase.', + 'Move data to the cold tier when you are searching it less often and don’t need to update it. The cold tier is optimized for cost savings over search performance.', }), frozen: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.frozenPhaseDescription', { defaultMessage: - 'Archive data as searchable snapshots in the frozen tier. The frozen tier is optimized for maximum cost savings. Data in the frozen tier is rarely accessed and never updated.', + 'Move data to the frozen tier for long term retention. The frozen tier provides the most cost-effective way store your data and still be able to search it.', } ), delete: i18n.translate( diff --git a/x-pack/plugins/infra/common/log_sources/errors.ts b/x-pack/plugins/infra/common/log_sources/errors.ts new file mode 100644 index 0000000000000..d715e8ea616cf --- /dev/null +++ b/x-pack/plugins/infra/common/log_sources/errors.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable max-classes-per-file */ + +export class ResolveLogSourceConfigurationError extends Error { + constructor(message: string, public cause?: Error) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'ResolveLogSourceConfigurationError'; + } +} + +export class FetchLogSourceConfigurationError extends Error { + constructor(message: string, public cause?: Error) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'FetchLogSourceConfigurationError'; + } +} + +export class FetchLogSourceStatusError extends Error { + constructor(message: string, public cause?: Error) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'FetchLogSourceStatusError'; + } +} + +export class PatchLogSourceConfigurationError extends Error { + constructor(message: string, public cause?: Error) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'PatchLogSourceConfigurationError'; + } +} diff --git a/x-pack/plugins/infra/common/log_sources/index.ts b/x-pack/plugins/infra/common/log_sources/index.ts index bc36c45307e4d..a2d200544f45e 100644 --- a/x-pack/plugins/infra/common/log_sources/index.ts +++ b/x-pack/plugins/infra/common/log_sources/index.ts @@ -5,5 +5,6 @@ * 2.0. */ +export * from './errors'; export * from './log_source_configuration'; export * from './resolved_log_source_configuration'; diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts index daac7f6a138eb..77c7947ce22c3 100644 --- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts +++ b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts @@ -8,6 +8,7 @@ import { estypes } from '@elastic/elasticsearch'; import { IndexPattern, IndexPatternsContract } from '../../../../../src/plugins/data/common'; import { ObjectEntries } from '../utility_types'; +import { ResolveLogSourceConfigurationError } from './errors'; import { LogSourceColumnConfiguration, LogSourceConfigurationProperties, @@ -44,10 +45,19 @@ const resolveLegacyReference = async ( throw new Error('This function can only resolve legacy references'); } - const fields = await indexPatternsService.getFieldsForWildcard({ - pattern: sourceConfiguration.logIndices.indexName, - allowNoIndex: true, - }); + const indices = sourceConfiguration.logIndices.indexName; + + const fields = await indexPatternsService + .getFieldsForWildcard({ + pattern: indices, + allowNoIndex: true, + }) + .catch((error) => { + throw new ResolveLogSourceConfigurationError( + `Failed to fetch fields for indices "${indices}": ${error}`, + error + ); + }); return { indices: sourceConfiguration.logIndices.indexName, @@ -70,9 +80,14 @@ const resolveKibanaIndexPatternReference = async ( throw new Error('This function can only resolve Kibana Index Pattern references'); } - const indexPattern = await indexPatternsService.get( - sourceConfiguration.logIndices.indexPatternId - ); + const { indexPatternId } = sourceConfiguration.logIndices; + + const indexPattern = await indexPatternsService.get(indexPatternId).catch((error) => { + throw new ResolveLogSourceConfigurationError( + `Failed to fetch index pattern "${indexPatternId}": ${error}`, + error + ); + }); return { indices: indexPattern.title, diff --git a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts index 40390d386f1c5..436432e9f0caf 100644 --- a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts +++ b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts @@ -160,12 +160,6 @@ export const SavedSourceConfigurationRuntimeType = rt.intersection([ export interface InfraSavedSourceConfiguration extends rt.TypeOf {} -export const pickSavedSourceConfiguration = ( - value: InfraSourceConfiguration -): InfraSavedSourceConfiguration => { - return value; -}; - /** * Static source configuration, the result of merging values from the config file and * hardcoded defaults. diff --git a/x-pack/plugins/infra/public/components/error_page.tsx b/x-pack/plugins/infra/public/components/error_page.tsx index 58be2788a3154..184901b4fdd9b 100644 --- a/x-pack/plugins/infra/public/components/error_page.tsx +++ b/x-pack/plugins/infra/public/components/error_page.tsx @@ -13,10 +13,10 @@ import { EuiPageBody, EuiPageContent, EuiPageContentBody, + EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; - import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { FlexPage } from './page'; @@ -45,7 +45,7 @@ export const ErrorPage: React.FC = ({ detailedMessage, retry, shortMessag /> } > - + {shortMessage} {retry ? ( @@ -58,7 +58,12 @@ export const ErrorPage: React.FC = ({ detailedMessage, retry, shortMessag ) : null} - {detailedMessage ?
{detailedMessage}
: null} + {detailedMessage ? ( + <> + +
{detailedMessage}
+ + ) : null} diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx index 5023f9d5d5fd4..44d78591fbf2f 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx @@ -111,10 +111,10 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re } const { - sourceConfiguration, - loadSourceConfiguration, - isLoadingSourceConfiguration, derivedIndexPattern, + isLoadingSourceConfiguration, + loadSource, + sourceConfiguration, } = useLogSource({ sourceId, fetch: services.http.fetch, @@ -164,8 +164,8 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re // Component lifetime useEffect(() => { - loadSourceConfiguration(); - }, [loadSourceConfiguration]); + loadSource(); + }, [loadSource]); useEffect(() => { fetchEntries(); diff --git a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx new file mode 100644 index 0000000000000..8ea35fd8f259f --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx @@ -0,0 +1,141 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiEmptyPrompt, + EuiPageTemplate, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { SavedObjectNotFound } from '../../../../../../src/plugins/kibana_utils/common'; +import { + FetchLogSourceConfigurationError, + FetchLogSourceStatusError, + ResolveLogSourceConfigurationError, +} from '../../../common/log_sources'; +import { useLinkProps } from '../../hooks/use_link_props'; + +export const LogSourceErrorPage: React.FC<{ + errors: Error[]; + onRetry: () => void; +}> = ({ errors, onRetry }) => { + const settingsLinkProps = useLinkProps({ app: 'logs', pathname: '/settings' }); + + return ( + + + + + } + body={ + <> +

+ +

+ {errors.map((error) => ( + + + + + ))} + + } + actions={[ + + + , + + + , + ]} + /> +
+ ); +}; + +const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => { + if (error instanceof ResolveLogSourceConfigurationError) { + return ( + + } + > + {error.cause instanceof SavedObjectNotFound ? ( + // the SavedObjectNotFound error message contains broken markup + + ) : ( + `${error.cause?.message ?? error.message}` + )} + + ); + } else if (error instanceof FetchLogSourceConfigurationError) { + return ( + + } + > + {`${error.cause?.message ?? error.message}`} + + ); + } else if (error instanceof FetchLogSourceStatusError) { + return ( + + } + > + {`${error.cause?.message ?? error.message}`} + + ); + } else { + return {`${error.message}`}; + } +}; + +const LogSourceErrorCallout: React.FC<{ title: React.ReactNode }> = ({ title, children }) => ( + +

{children}

+
+); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts index 1a7405d0569bd..d46668e7a3db3 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts @@ -10,12 +10,24 @@ import { getLogSourceConfigurationPath, getLogSourceConfigurationSuccessResponsePayloadRT, } from '../../../../../common/http_api/log_sources'; +import { FetchLogSourceConfigurationError } from '../../../../../common/log_sources'; import { decodeOrThrow } from '../../../../../common/runtime_types'; export const callFetchLogSourceConfigurationAPI = async (sourceId: string, fetch: HttpHandler) => { const response = await fetch(getLogSourceConfigurationPath(sourceId), { method: 'GET', + }).catch((error) => { + throw new FetchLogSourceConfigurationError( + `Failed to fetch log source configuration "${sourceId}": ${error}`, + error + ); }); - return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response); + return decodeOrThrow( + getLogSourceConfigurationSuccessResponsePayloadRT, + (message: string) => + new FetchLogSourceConfigurationError( + `Failed to decode log source configuration "${sourceId}": ${message}` + ) + )(response); }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts index 76a9549df611c..38e4378b88571 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts @@ -10,12 +10,24 @@ import { getLogSourceStatusPath, getLogSourceStatusSuccessResponsePayloadRT, } from '../../../../../common/http_api/log_sources'; +import { FetchLogSourceStatusError } from '../../../../../common/log_sources'; import { decodeOrThrow } from '../../../../../common/runtime_types'; export const callFetchLogSourceStatusAPI = async (sourceId: string, fetch: HttpHandler) => { const response = await fetch(getLogSourceStatusPath(sourceId), { method: 'GET', + }).catch((error) => { + throw new FetchLogSourceStatusError( + `Failed to fetch status for log source "${sourceId}": ${error}`, + error + ); }); - return decodeOrThrow(getLogSourceStatusSuccessResponsePayloadRT)(response); + return decodeOrThrow( + getLogSourceStatusSuccessResponsePayloadRT, + (message: string) => + new FetchLogSourceStatusError( + `Failed to decode status for log source "${sourceId}": ${message}` + ) + )(response); }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts index 2b07a92c05b08..f469d2ab33421 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts @@ -12,6 +12,7 @@ import { patchLogSourceConfigurationRequestBodyRT, LogSourceConfigurationPropertiesPatch, } from '../../../../../common/http_api/log_sources'; +import { PatchLogSourceConfigurationError } from '../../../../../common/log_sources'; import { decodeOrThrow } from '../../../../../common/runtime_types'; export const callPatchLogSourceConfigurationAPI = async ( @@ -26,7 +27,18 @@ export const callPatchLogSourceConfigurationAPI = async ( data: patchedProperties, }) ), + }).catch((error) => { + throw new PatchLogSourceConfigurationError( + `Failed to update log source configuration "${sourceId}": ${error}`, + error + ); }); - return decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response); + return decodeOrThrow( + patchLogSourceConfigurationSuccessResponsePayloadRT, + (message: string) => + new PatchLogSourceConfigurationError( + `Failed to decode log source configuration "${sourceId}": ${message}` + ) + )(response); }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts index 7e23f51c1c562..bda1085d44612 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts @@ -18,9 +18,10 @@ export const createUninitializedUseLogSourceMock: CreateUseLogSource = ({ fields: [], title: 'unknown', }, + hasFailedLoading: false, hasFailedLoadingSource: false, hasFailedLoadingSourceStatus: false, - hasFailedResolvingSourceConfiguration: false, + hasFailedResolvingSource: false, initialize: jest.fn(), isLoading: false, isLoadingSourceConfiguration: false, @@ -29,13 +30,13 @@ export const createUninitializedUseLogSourceMock: CreateUseLogSource = ({ isUninitialized: true, loadSource: jest.fn(), loadSourceConfiguration: jest.fn(), - loadSourceFailureMessage: undefined, + latestLoadSourceFailures: [], resolveSourceFailureMessage: undefined, loadSourceStatus: jest.fn(), sourceConfiguration: undefined, sourceId, sourceStatus: undefined, - updateSourceConfiguration: jest.fn(), + updateSource: jest.fn(), resolvedSourceConfiguration: undefined, loadResolveLogSourceConfiguration: jest.fn(), }); @@ -83,6 +84,6 @@ export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfi }, }); -export const createAvailableSourceStatus = (logIndexFields = []): LogSourceStatus => ({ +export const createAvailableSourceStatus = (): LogSourceStatus => ({ logIndexStatus: 'available', }); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 81d650fcef35c..198d0d2efe44c 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -7,8 +7,8 @@ import createContainer from 'constate'; import { useCallback, useMemo, useState } from 'react'; -import useMountedState from 'react-use/lib/useMountedState'; import type { HttpHandler } from 'src/core/public'; +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common'; import { LogIndexField, LogSourceConfigurationPropertiesPatch, @@ -19,12 +19,12 @@ import { LogSourceConfigurationProperties, ResolvedLogSourceConfiguration, resolveLogSourceConfiguration, + ResolveLogSourceConfigurationError, } from '../../../../common/log_sources'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { isRejectedPromiseState, useTrackedPromise } from '../../../utils/use_tracked_promise'; import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration'; import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status'; import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common'; export { LogIndexField, @@ -32,6 +32,7 @@ export { LogSourceConfigurationProperties, LogSourceConfigurationPropertiesPatch, LogSourceStatus, + ResolveLogSourceConfigurationError, }; export const useLogSource = ({ @@ -43,7 +44,6 @@ export const useLogSource = ({ fetch: HttpHandler; indexPatternsService: IndexPatternsContract; }) => { - const getIsMounted = useMountedState(); const [sourceConfiguration, setSourceConfiguration] = useState< LogSourceConfiguration | undefined >(undefined); @@ -58,52 +58,34 @@ export const useLogSource = ({ { cancelPreviousOn: 'resolution', createPromise: async () => { - const { data: sourceConfigurationResponse } = await callFetchLogSourceConfigurationAPI( - sourceId, - fetch - ); - const resolvedSourceConfigurationResponse = await resolveLogSourceConfiguration( - sourceConfigurationResponse?.configuration, - indexPatternsService - ); - return { sourceConfigurationResponse, resolvedSourceConfigurationResponse }; - }, - onResolve: ({ sourceConfigurationResponse, resolvedSourceConfigurationResponse }) => { - if (!getIsMounted()) { - return; - } - - setSourceConfiguration(sourceConfigurationResponse); - setResolvedSourceConfiguration(resolvedSourceConfigurationResponse); + return (await callFetchLogSourceConfigurationAPI(sourceId, fetch)).data; }, + onResolve: setSourceConfiguration, }, [sourceId, fetch, indexPatternsService] ); - const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( + const [resolveSourceConfigurationRequest, resolveSourceConfiguration] = useTrackedPromise( { cancelPreviousOn: 'resolution', - createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { - const { data: updatedSourceConfig } = await callPatchLogSourceConfigurationAPI( - sourceId, - patchedProperties, - fetch - ); - const resolvedSourceConfig = await resolveLogSourceConfiguration( - updatedSourceConfig.configuration, + createPromise: async (unresolvedSourceConfiguration: LogSourceConfigurationProperties) => { + return await resolveLogSourceConfiguration( + unresolvedSourceConfiguration, indexPatternsService ); - return { updatedSourceConfig, resolvedSourceConfig }; }, - onResolve: ({ updatedSourceConfig, resolvedSourceConfig }) => { - if (!getIsMounted()) { - return; - } - - setSourceConfiguration(updatedSourceConfig); - setResolvedSourceConfiguration(resolvedSourceConfig); - loadSourceStatus(); + onResolve: setResolvedSourceConfiguration, + }, + [indexPatternsService] + ); + + const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { + return (await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch)).data; }, + onResolve: setSourceConfiguration, }, [sourceId, fetch, indexPatternsService] ); @@ -114,13 +96,7 @@ export const useLogSource = ({ createPromise: async () => { return await callFetchLogSourceStatusAPI(sourceId, fetch); }, - onResolve: ({ data }) => { - if (!getIsMounted()) { - return; - } - - setSourceStatus(data); - }, + onResolve: ({ data }) => setSourceStatus(data), }, [sourceId, fetch] ); @@ -133,53 +109,67 @@ export const useLogSource = ({ [resolvedSourceConfiguration] ); - const isLoadingSourceConfiguration = useMemo( - () => loadSourceConfigurationRequest.state === 'pending', - [loadSourceConfigurationRequest.state] - ); - - const isUpdatingSourceConfiguration = useMemo( - () => updateSourceConfigurationRequest.state === 'pending', - [updateSourceConfigurationRequest.state] - ); - - const isLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'pending', [ - loadSourceStatusRequest.state, - ]); - - const isLoading = useMemo( - () => isLoadingSourceConfiguration || isLoadingSourceStatus || isUpdatingSourceConfiguration, - [isLoadingSourceConfiguration, isLoadingSourceStatus, isUpdatingSourceConfiguration] - ); - - const isUninitialized = useMemo( - () => - loadSourceConfigurationRequest.state === 'uninitialized' || - loadSourceStatusRequest.state === 'uninitialized', - [loadSourceConfigurationRequest.state, loadSourceStatusRequest.state] - ); - - const hasFailedLoadingSource = useMemo( - () => loadSourceConfigurationRequest.state === 'rejected', - [loadSourceConfigurationRequest.state] - ); - - const hasFailedLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'rejected', [ - loadSourceStatusRequest.state, - ]); - - const loadSourceFailureMessage = useMemo( - () => - loadSourceConfigurationRequest.state === 'rejected' - ? `${loadSourceConfigurationRequest.value}` - : undefined, - [loadSourceConfigurationRequest] + const isLoadingSourceConfiguration = loadSourceConfigurationRequest.state === 'pending'; + const isResolvingSourceConfiguration = resolveSourceConfigurationRequest.state === 'pending'; + const isLoadingSourceStatus = loadSourceStatusRequest.state === 'pending'; + const isUpdatingSourceConfiguration = updateSourceConfigurationRequest.state === 'pending'; + + const isLoading = + isLoadingSourceConfiguration || + isResolvingSourceConfiguration || + isLoadingSourceStatus || + isUpdatingSourceConfiguration; + + const isUninitialized = + loadSourceConfigurationRequest.state === 'uninitialized' || + resolveSourceConfigurationRequest.state === 'uninitialized' || + loadSourceStatusRequest.state === 'uninitialized'; + + const hasFailedLoadingSource = loadSourceConfigurationRequest.state === 'rejected'; + const hasFailedResolvingSource = resolveSourceConfigurationRequest.state === 'rejected'; + const hasFailedLoadingSourceStatus = loadSourceStatusRequest.state === 'rejected'; + + const latestLoadSourceFailures = [ + loadSourceConfigurationRequest, + resolveSourceConfigurationRequest, + loadSourceStatusRequest, + ] + .filter(isRejectedPromiseState) + .map(({ value }) => (value instanceof Error ? value : new Error(`${value}`))); + + const hasFailedLoading = latestLoadSourceFailures.length > 0; + + const loadSource = useCallback(async () => { + const loadSourceConfigurationPromise = loadSourceConfiguration(); + const loadSourceStatusPromise = loadSourceStatus(); + const resolveSourceConfigurationPromise = resolveSourceConfiguration( + (await loadSourceConfigurationPromise).configuration + ); + + return await Promise.all([ + loadSourceConfigurationPromise, + resolveSourceConfigurationPromise, + loadSourceStatusPromise, + ]); + }, [loadSourceConfiguration, loadSourceStatus, resolveSourceConfiguration]); + + const updateSource = useCallback( + async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { + const updatedSourceConfiguration = await updateSourceConfiguration(patchedProperties); + const resolveSourceConfigurationPromise = resolveSourceConfiguration( + updatedSourceConfiguration.configuration + ); + const loadSourceStatusPromise = loadSourceStatus(); + + return await Promise.all([ + updatedSourceConfiguration, + resolveSourceConfigurationPromise, + loadSourceStatusPromise, + ]); + }, + [loadSourceStatus, resolveSourceConfiguration, updateSourceConfiguration] ); - const loadSource = useCallback(() => { - return Promise.all([loadSourceConfiguration(), loadSourceStatus()]); - }, [loadSourceConfiguration, loadSourceStatus]); - const initialize = useCallback(async () => { if (!isUninitialized) { return; @@ -194,21 +184,23 @@ export const useLogSource = ({ isUninitialized, derivedIndexPattern, // Failure states + hasFailedLoading, hasFailedLoadingSource, hasFailedLoadingSourceStatus, - loadSourceFailureMessage, + hasFailedResolvingSource, + latestLoadSourceFailures, // Loading states isLoading, isLoadingSourceConfiguration, isLoadingSourceStatus, + isResolvingSourceConfiguration, // Source status (denotes the state of the indices, e.g. missing) sourceStatus, loadSourceStatus, // Source configuration (represents the raw attributes of the source configuration) loadSource, - loadSourceConfiguration, sourceConfiguration, - updateSourceConfiguration, + updateSource, // Resolved source configuration (represents a fully resolved state, you would use this for the vast majority of "read" scenarios) resolvedSourceConfiguration, }; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 0df8e639b149b..82e3813bde886 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -36,7 +36,7 @@ export const RedirectToNodeLogs = ({ location, }: RedirectToNodeLogsType) => { const { services } = useKibanaContextForPlugin(); - const { isLoading, loadSourceConfiguration, sourceConfiguration } = useLogSource({ + const { isLoading, loadSource, sourceConfiguration } = useLogSource({ fetch: services.http.fetch, sourceId, indexPatternsService: services.data.indexPatterns, @@ -44,7 +44,7 @@ export const RedirectToNodeLogs = ({ const fields = sourceConfiguration?.configuration.fields; useMount(() => { - loadSourceConfiguration(); + loadSource(); }); if (isLoading) { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index 628df397998ee..1762caed14a67 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect } from 'react'; -import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { @@ -19,23 +18,13 @@ import { LogAnalysisSetupFlyout, useLogAnalysisSetupFlyoutStateContext, } from '../../../components/logging/log_analysis_setup/setup_flyout'; -import { SourceErrorPage } from '../../../components/source_error_page'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryCategoriesResultsContent } from './page_results_content'; import { LogEntryCategoriesSetupContent } from './page_setup_content'; export const LogEntryCategoriesPageContent = () => { - const { - hasFailedLoadingSource, - isLoading, - isUninitialized, - loadSource, - loadSourceFailureMessage, - } = useLogSourceContext(); - const { hasLogAnalysisCapabilites, hasLogAnalysisReadCapabilities, @@ -55,11 +44,7 @@ export const LogEntryCategoriesPageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); - if (isLoading || isUninitialized) { - return ; - } else if (hasFailedLoadingSource) { - return ; - } else if (!hasLogAnalysisCapabilites) { + if (!hasLogAnalysisCapabilites) { return ; } else if (!hasLogAnalysisReadCapabilities) { return ; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx index ab409d661fe0a..1eed4b6af65e8 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx @@ -7,30 +7,46 @@ import React from 'react'; import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout'; +import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, resolvedSourceConfiguration } = useLogSourceContext(); + const { + hasFailedLoading, + isLoading, + isUninitialized, + latestLoadSourceFailures, + loadSource, + resolvedSourceConfiguration, + sourceId, + } = useLogSourceContext(); const { space } = useActiveKibanaSpace(); // This is a rather crude way of guarding the dependent providers against // arguments that are only made available asynchronously. Ideally, we'd use // React concurrent mode and Suspense in order to handle that more gracefully. - if (!resolvedSourceConfiguration || space == null) { + if (space == null) { + return null; + } else if (hasFailedLoading) { + return ; + } else if (isLoading || isUninitialized) { + return ; + } else if (resolvedSourceConfiguration != null) { + return ( + + {children} + + ); + } else { return null; } - - return ( - - {children} - - ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 114f8ff9db3b3..061a2ba0acc1d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -6,9 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import React, { memo, useEffect, useCallback } from 'react'; +import React, { memo, useCallback, useEffect } from 'react'; import useInterval from 'react-use/lib/useInterval'; -import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { @@ -20,26 +19,16 @@ import { LogAnalysisSetupFlyout, useLogAnalysisSetupFlyoutStateContext, } from '../../../components/logging/log_analysis_setup/setup_flyout'; -import { SourceErrorPage } from '../../../components/source_error_page'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryRateResultsContent } from './page_results_content'; import { LogEntryRateSetupContent } from './page_setup_content'; const JOB_STATUS_POLLING_INTERVAL = 30000; export const LogEntryRatePageContent = memo(() => { - const { - hasFailedLoadingSource, - isLoading, - isUninitialized, - loadSource, - loadSourceFailureMessage, - } = useLogSourceContext(); - const { hasLogAnalysisCapabilites, hasLogAnalysisReadCapabilities, @@ -93,11 +82,7 @@ export const LogEntryRatePageContent = memo(() => { } }, JOB_STATUS_POLLING_INTERVAL); - if (isLoading || isUninitialized) { - return ; - } else if (hasFailedLoadingSource) { - return ; - } else if (!hasLogAnalysisCapabilites) { + if (!hasLogAnalysisCapabilites) { return ; } else if (!hasLogAnalysisReadCapabilities) { return ; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index 628e2fb74d830..043ed2501c973 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -7,42 +7,58 @@ import React from 'react'; import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout'; +import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; +import { LogFlyout } from '../../../containers/logs/log_flyout'; import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; -import { LogFlyout } from '../../../containers/logs/log_flyout'; export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, resolvedSourceConfiguration } = useLogSourceContext(); + const { + hasFailedLoading, + isLoading, + isUninitialized, + latestLoadSourceFailures, + loadSource, + resolvedSourceConfiguration, + sourceId, + } = useLogSourceContext(); const { space } = useActiveKibanaSpace(); // This is a rather crude way of guarding the dependent providers against // arguments that are only made available asynchronously. Ideally, we'd use // React concurrent mode and Suspense in order to handle that more gracefully. - if (!resolvedSourceConfiguration || space == null) { + if (space == null) { return null; - } - - return ( - - - ; + } else if (hasFailedLoading) { + return ; + } else if (resolvedSourceConfiguration != null) { + return ( + + - {children} - - - - ); + + {children} + + + + ); + } else { + return null; + } }; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx index 9e110db53a27f..b91119b7d5625 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx @@ -28,15 +28,30 @@ export const IndexPatternSelector: React.FC<{ fetchIndexPatternTitles(); }, [fetchIndexPatternTitles]); - const availableOptions = useMemo( - () => - availableIndexPatterns.map(({ id, title }) => ({ + const availableOptions = useMemo(() => { + const options = [ + ...availableIndexPatterns.map(({ id, title }) => ({ key: id, label: title, value: id, })), - [availableIndexPatterns] - ); + ...(indexPatternId == null || availableIndexPatterns.some(({ id }) => id === indexPatternId) + ? [] + : [ + { + key: indexPatternId, + label: i18n.translate('xpack.infra.logSourceConfiguration.missingIndexPatternLabel', { + defaultMessage: `Missing index pattern {indexPatternId}`, + values: { + indexPatternId, + }, + }), + value: indexPatternId, + }, + ]), + ]; + return options; + }, [availableIndexPatterns, indexPatternId]); const selectedOptions = useMemo( () => availableOptions.filter(({ key }) => key === indexPatternId), diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts index 49d14e04ca328..1a70aaff6636c 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import { SavedObjectNotFound } from '../../../../../../../src/plugins/kibana_utils/common'; import { useUiTracker } from '../../../../../observability/public'; import { LogIndexNameReference, @@ -45,9 +46,20 @@ export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => { return emptyStringErrors; } - const indexPatternErrors = validateIndexPattern( - await indexPatternService.get(logIndices.indexPatternId) - ); + const indexPatternErrors = await indexPatternService + .get(logIndices.indexPatternId) + .then(validateIndexPattern, (error): FormValidationError[] => { + if (error instanceof SavedObjectNotFound) { + return [ + { + type: 'missing_index_pattern' as const, + indexPatternId: logIndices.indexPatternId, + }, + ]; + } else { + throw error; + } + }); if (indexPatternErrors.length > 0) { trackIndexPatternValidationError({ diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx index af36a9dc0090b..37262e05db5a0 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx @@ -88,6 +88,16 @@ export const LogSourceConfigurationFormError: React.FC<{ error: FormValidationEr defaultMessage="The index pattern must not be a rollup index pattern." /> ); + } else if (error.type === 'missing_index_pattern') { + return ( + {error.indexPatternId}, + }} + /> + ); } else { return null; } diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx index 9ab7d38e6c838..b295a392c8df9 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -43,9 +43,10 @@ export const LogsSettingsPage = () => { const { sourceConfiguration: source, + hasFailedLoadingSource, isLoading, isUninitialized, - updateSourceConfiguration, + updateSource, resolvedSourceConfiguration, } = useLogSourceContext(); @@ -65,9 +66,9 @@ export const LogsSettingsPage = () => { } = useLogSourceConfigurationFormState(source?.configuration); const persistUpdates = useCallback(async () => { - await updateSourceConfiguration(formState); + await updateSource(formState); sourceConfigurationFormElement.resetValue(); - }, [updateSourceConfiguration, sourceConfigurationFormElement, formState]); + }, [updateSource, sourceConfigurationFormElement, formState]); const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ shouldAllowEdit, @@ -77,7 +78,7 @@ export const LogsSettingsPage = () => { if ((isLoading || isUninitialized) && !resolvedSourceConfiguration) { return ; } - if (!source?.configuration) { + if (hasFailedLoadingSource) { return null; } diff --git a/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts b/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts index b6e5a387590ed..81b9297f8a70b 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts +++ b/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts @@ -45,6 +45,11 @@ export interface RollupIndexPatternValidationError { indexPatternTitle: string; } +export interface MissingIndexPatternValidationError { + type: 'missing_index_pattern'; + indexPatternId: string; +} + export type FormValidationError = | GenericValidationError | ChildFormValidationError @@ -53,7 +58,8 @@ export type FormValidationError = | MissingTimestampFieldValidationError | MissingMessageFieldValidationError | InvalidMessageFieldTypeValidationError - | RollupIndexPatternValidationError; + | RollupIndexPatternValidationError + | MissingIndexPatternValidationError; export const validateStringNotEmpty = (fieldName: string, value: string): FormValidationError[] => value === '' ? [{ type: 'empty_field', fieldName }] : []; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index f04d4c38f5e79..5ff07e713233a 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -6,26 +6,26 @@ */ import React from 'react'; -import { SourceErrorPage } from '../../../components/source_error_page'; +import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogsPageLogsContent } from './page_logs_content'; import { LogsPageNoIndicesContent } from './page_no_indices_content'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; export const StreamPageContent: React.FunctionComponent = () => { const { - hasFailedLoadingSource, + hasFailedLoading, isLoading, isUninitialized, loadSource, - loadSourceFailureMessage, + latestLoadSourceFailures, sourceStatus, } = useLogSourceContext(); if (isLoading || isUninitialized) { return ; - } else if (hasFailedLoadingSource) { - return ; + } else if (hasFailedLoading) { + return ; } else if (sourceStatus?.logIndexStatus !== 'missing') { return ; } else { diff --git a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts index 8d9980be01bba..1b0c290bd6511 100644 --- a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts +++ b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts @@ -256,14 +256,18 @@ export interface RejectedPromiseState { value: RejectedValue; } -type SettledPromise = +export type SettledPromiseState = | ResolvedPromiseState | RejectedPromiseState; -type PromiseState = +export type PromiseState = | UninitializedPromiseState | PendingPromiseState - | SettledPromise; + | SettledPromiseState; + +export const isRejectedPromiseState = ( + promiseState: PromiseState +): promiseState is RejectedPromiseState => promiseState.state === 'rejected'; interface CancelablePromise { // reject the promise prematurely with a CanceledPromiseError diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 8cee4ea588722..cf3d8a15b7b65 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -46,7 +46,7 @@ export const evaluateCondition = async ({ condition: InventoryMetricConditions; nodeType: InventoryItemType; source: InfraSource; - logQueryFields: LogQueryFields; + logQueryFields: LogQueryFields | undefined; esClient: ElasticsearchClient; compositeSize: number; filterQuery?: string; @@ -115,7 +115,7 @@ const getData = async ( metric: SnapshotMetricType, timerange: InfraTimerangeInput, source: InfraSource, - logQueryFields: LogQueryFields, + logQueryFields: LogQueryFields | undefined, compositeSize: number, filterQuery?: string, customMetric?: SnapshotCustomMetricInput @@ -144,8 +144,8 @@ const getData = async ( client, snapshotRequest, source, - logQueryFields, - compositeSize + compositeSize, + logQueryFields ); if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 0db6a9d83c852..7a890ac14482a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -68,11 +68,13 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = sourceId || 'default' ); - const logQueryFields = await libs.getLogQueryFields( - sourceId || 'default', - services.savedObjectsClient, - services.scopedClusterClient.asCurrentUser - ); + const logQueryFields = await libs + .getLogQueryFields( + sourceId || 'default', + services.savedObjectsClient, + services.scopedClusterClient.asCurrentUser + ) + .catch(() => undefined); const compositeSize = libs.configuration.inventory.compositeSize; diff --git a/x-pack/plugins/infra/server/lib/sources/errors.ts b/x-pack/plugins/infra/server/lib/sources/errors.ts index 082dfc611cc5b..b99e77f238c65 100644 --- a/x-pack/plugins/infra/server/lib/sources/errors.ts +++ b/x-pack/plugins/infra/server/lib/sources/errors.ts @@ -4,7 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + /* eslint-disable max-classes-per-file */ + export class NotFoundError extends Error { constructor(message?: string) { super(message); @@ -18,3 +20,11 @@ export class AnomalyThresholdRangeError extends Error { Object.setPrototypeOf(this, new.target.prototype); } } + +export class SavedObjectReferenceResolutionError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'SavedObjectReferenceResolutionError'; + } +} diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts new file mode 100644 index 0000000000000..7d31f7342b05b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration'; +import { + extractSavedObjectReferences, + resolveSavedObjectReferences, +} from './saved_object_references'; + +describe('extractSavedObjectReferences function', () => { + it('extracts log index pattern references', () => { + const { attributes, references } = extractSavedObjectReferences( + sourceConfigurationWithIndexPatternReference + ); + + expect(references).toMatchObject([{ id: 'INDEX_PATTERN_ID' }]); + expect(attributes).toHaveProperty(['logIndices', 'indexPatternId'], references[0].name); + }); + + it('ignores log index name references', () => { + const { attributes, references } = extractSavedObjectReferences( + sourceConfigurationWithIndexNameReference + ); + + expect(references).toHaveLength(0); + expect(attributes).toHaveProperty(['logIndices', 'indexName'], 'INDEX_NAME'); + }); +}); + +describe('resolveSavedObjectReferences function', () => { + it('is the inverse operation of extractSavedObjectReferences', () => { + const { attributes, references } = extractSavedObjectReferences( + sourceConfigurationWithIndexPatternReference + ); + + const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, references); + + expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexPatternReference); + }); + + it('ignores additional saved object references', () => { + const { attributes, references } = extractSavedObjectReferences( + sourceConfigurationWithIndexPatternReference + ); + + const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, [ + ...references, + { name: 'log_index_pattern_1', id: 'SOME_ID', type: 'index-pattern' }, + ]); + + expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexPatternReference); + }); + + it('ignores log index name references', () => { + const { attributes, references } = extractSavedObjectReferences( + sourceConfigurationWithIndexNameReference + ); + + const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, [ + ...references, + { name: 'log_index_pattern_0', id: 'SOME_ID', type: 'index-pattern' }, + ]); + + expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexNameReference); + }); +}); + +const sourceConfigurationWithIndexPatternReference: InfraSourceConfiguration = { + name: 'NAME', + description: 'DESCRIPTION', + fields: { + container: 'CONTAINER_FIELD', + host: 'HOST_FIELD', + message: ['MESSAGE_FIELD'], + pod: 'POD_FIELD', + tiebreaker: 'TIEBREAKER_FIELD', + timestamp: 'TIMESTAMP_FIELD', + }, + logColumns: [], + logIndices: { + type: 'index_pattern', + indexPatternId: 'INDEX_PATTERN_ID', + }, + metricAlias: 'METRIC_ALIAS', + anomalyThreshold: 0, + inventoryDefaultView: 'INVENTORY_DEFAULT_VIEW', + metricsExplorerDefaultView: 'METRICS_EXPLORER_DEFAULT_VIEW', +}; + +const sourceConfigurationWithIndexNameReference: InfraSourceConfiguration = { + ...sourceConfigurationWithIndexPatternReference, + logIndices: { + type: 'index_name', + indexName: 'INDEX_NAME', + }, +}; diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts new file mode 100644 index 0000000000000..31f36380cc23e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference } from 'src/core/server'; +import { + InfraSavedSourceConfiguration, + InfraSourceConfiguration, +} from '../../../common/source_configuration/source_configuration'; +import { SavedObjectReferenceResolutionError } from './errors'; + +const logIndexPatternReferenceName = 'log_index_pattern_0'; + +interface SavedObjectAttributesWithReferences { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; +} + +/** + * Rewrites a source configuration such that well-known saved object references + * are extracted in the `references` array and replaced by the appropriate + * name. This is the inverse operation to `resolveSavedObjectReferences`. + */ +export const extractSavedObjectReferences = ( + sourceConfiguration: InfraSourceConfiguration +): SavedObjectAttributesWithReferences => + [extractLogIndicesSavedObjectReferences].reduce< + SavedObjectAttributesWithReferences + >( + ({ attributes: accumulatedAttributes, references: accumulatedReferences }, extract) => { + const { attributes, references } = extract(accumulatedAttributes); + return { + attributes, + references: [...accumulatedReferences, ...references], + }; + }, + { + attributes: sourceConfiguration, + references: [], + } + ); + +/** + * Rewrites a source configuration such that well-known saved object references + * are resolved from the `references` argument and replaced by the real saved + * object ids. This is the inverse operation to `extractSavedObjectReferences`. + */ +export const resolveSavedObjectReferences = ( + attributes: InfraSavedSourceConfiguration, + references: SavedObjectReference[] +): InfraSavedSourceConfiguration => + [resolveLogIndicesSavedObjectReferences].reduce( + (accumulatedAttributes, resolve) => resolve(accumulatedAttributes, references), + attributes + ); + +const extractLogIndicesSavedObjectReferences = ( + sourceConfiguration: InfraSourceConfiguration +): SavedObjectAttributesWithReferences => { + if (sourceConfiguration.logIndices.type === 'index_pattern') { + const logIndexPatternReference: SavedObjectReference = { + id: sourceConfiguration.logIndices.indexPatternId, + type: 'index-pattern', + name: logIndexPatternReferenceName, + }; + const attributes: InfraSourceConfiguration = { + ...sourceConfiguration, + logIndices: { + ...sourceConfiguration.logIndices, + indexPatternId: logIndexPatternReference.name, + }, + }; + return { + attributes, + references: [logIndexPatternReference], + }; + } else { + return { + attributes: sourceConfiguration, + references: [], + }; + } +}; + +const resolveLogIndicesSavedObjectReferences = ( + attributes: InfraSavedSourceConfiguration, + references: SavedObjectReference[] +): InfraSavedSourceConfiguration => { + if (attributes.logIndices?.type === 'index_pattern') { + const logIndexPatternReference = references.find( + (reference) => reference.name === logIndexPatternReferenceName + ); + + if (logIndexPatternReference == null) { + throw new SavedObjectReferenceResolutionError( + `Failed to resolve log index pattern reference "${logIndexPatternReferenceName}".` + ); + } + + return { + ...attributes, + logIndices: { + ...attributes.logIndices, + indexPatternId: logIndexPatternReference.id, + }, + }; + } else { + return attributes; + } +}; diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index e5807322b87fc..904f51d12673f 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { SavedObject } from '../../../../../../src/core/server'; +import { infraSourceConfigurationSavedObjectName } from './saved_object_type'; import { InfraSources } from './sources'; describe('the InfraSources lib', () => { @@ -18,9 +20,10 @@ describe('the InfraSources lib', () => { id: 'TEST_ID', version: 'foo', updated_at: '2000-01-01T00:00:00.000Z', + type: infraSourceConfigurationSavedObjectName, attributes: { metricAlias: 'METRIC_ALIAS', - logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' }, + logIndices: { type: 'index_pattern', indexPatternId: 'log_index_pattern_0' }, fields: { container: 'CONTAINER', host: 'HOST', @@ -29,6 +32,13 @@ describe('the InfraSources lib', () => { timestamp: 'TIMESTAMP', }, }, + references: [ + { + id: 'LOG_INDEX_PATTERN', + name: 'log_index_pattern_0', + type: 'index-pattern', + }, + ], }); expect( @@ -39,7 +49,7 @@ describe('the InfraSources lib', () => { updatedAt: 946684800000, configuration: { metricAlias: 'METRIC_ALIAS', - logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' }, + logIndices: { type: 'index_pattern', indexPatternId: 'LOG_INDEX_PATTERN' }, fields: { container: 'CONTAINER', host: 'HOST', @@ -70,12 +80,14 @@ describe('the InfraSources lib', () => { const request: any = createRequestContext({ id: 'TEST_ID', version: 'foo', + type: infraSourceConfigurationSavedObjectName, updated_at: '2000-01-01T00:00:00.000Z', attributes: { fields: { container: 'CONTAINER', }, }, + references: [], }); expect( @@ -106,8 +118,10 @@ describe('the InfraSources lib', () => { const request: any = createRequestContext({ id: 'TEST_ID', version: 'foo', + type: infraSourceConfigurationSavedObjectName, updated_at: '2000-01-01T00:00:00.000Z', attributes: {}, + references: [], }); expect( @@ -140,7 +154,7 @@ const createMockStaticConfiguration = (sources: any) => ({ sources, }); -const createRequestContext = (savedObject?: any) => { +const createRequestContext = (savedObject?: SavedObject) => { return { core: { savedObjects: { diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 24b204665c014..7dc47388bd1da 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -5,26 +5,29 @@ * 2.0. */ -import { failure } from 'io-ts/lib/PathReporter'; -import { identity, constant } from 'fp-ts/lib/function'; +import { fold, map } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { map, fold } from 'fp-ts/lib/Either'; +import { failure } from 'io-ts/lib/PathReporter'; import { inRange } from 'lodash'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { defaultSourceConfiguration } from './defaults'; -import { AnomalyThresholdRangeError, NotFoundError } from './errors'; -import { infraSourceConfigurationSavedObjectName } from './saved_object_type'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import { InfraSavedSourceConfiguration, + InfraSource, InfraSourceConfiguration, InfraStaticSourceConfiguration, - pickSavedSourceConfiguration, - SourceConfigurationSavedObjectRuntimeType, - InfraSource, - sourceConfigurationConfigFilePropertiesRT, SourceConfigurationConfigFileProperties, + sourceConfigurationConfigFilePropertiesRT, + SourceConfigurationSavedObjectRuntimeType, } from '../../../common/source_configuration/source_configuration'; import { InfraConfig } from '../../../server'; +import { defaultSourceConfiguration } from './defaults'; +import { AnomalyThresholdRangeError, NotFoundError } from './errors'; +import { + extractSavedObjectReferences, + resolveSavedObjectReferences, +} from './saved_object_references'; +import { infraSourceConfigurationSavedObjectName } from './saved_object_type'; interface Libs { config: InfraConfig; @@ -113,13 +116,13 @@ export class InfraSources { staticDefaultSourceConfiguration, source ); + const { attributes, references } = extractSavedObjectReferences(newSourceConfiguration); const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await savedObjectsClient.create( - infraSourceConfigurationSavedObjectName, - pickSavedSourceConfiguration(newSourceConfiguration) as any, - { id: sourceId } - ) + await savedObjectsClient.create(infraSourceConfigurationSavedObjectName, attributes, { + id: sourceId, + references, + }) ); return { @@ -158,19 +161,19 @@ export class InfraSources { configuration, sourceProperties ); + const { attributes, references } = extractSavedObjectReferences( + updatedSourceConfigurationAttributes + ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( // update() will perform a deep merge. We use create() with overwrite: true instead. mergeSourceConfiguration() // ensures the correct and intended merging of properties. - await savedObjectsClient.create( - infraSourceConfigurationSavedObjectName, - pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, - { - id: sourceId, - version, - overwrite: true, - } - ) + await savedObjectsClient.create(infraSourceConfigurationSavedObjectName, attributes, { + id: sourceId, + overwrite: true, + references, + version, + }) ); return { @@ -267,7 +270,7 @@ const mergeSourceConfiguration = ( first ); -export const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknown) => +export const convertSavedObjectToSavedSourceConfiguration = (savedObject: SavedObject) => pipe( SourceConfigurationSavedObjectRuntimeType.decode(savedObject), map((savedSourceConfiguration) => ({ @@ -275,7 +278,10 @@ export const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknow version: savedSourceConfiguration.version, updatedAt: savedSourceConfiguration.updated_at, origin: 'stored' as 'stored', - configuration: savedSourceConfiguration.attributes, + configuration: resolveSavedObjectReferences( + savedSourceConfiguration.attributes, + savedObject.references + ), })), fold((errors) => { throw new Error(failure(errors).join('\n')); diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index 846fabcfa4e68..b86eb9f7d4c95 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -41,11 +41,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { snapshotRequest.sourceId ); const compositeSize = libs.configuration.inventory.compositeSize; - const logQueryFields = await libs.getLogQueryFields( - snapshotRequest.sourceId, - requestContext.core.savedObjects.client, - requestContext.core.elasticsearch.client.asCurrentUser - ); + const logQueryFields = await libs + .getLogQueryFields( + snapshotRequest.sourceId, + requestContext.core.savedObjects.client, + requestContext.core.elasticsearch.client.asCurrentUser + ) + .catch(() => undefined); UsageCollector.countNode(snapshotRequest.nodeType); const client = createSearchClient(requestContext, framework); @@ -55,8 +57,8 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { client, snapshotRequest, source, - logQueryFields, - compositeSize + compositeSize, + logQueryFields ); return response.ok({ body: SnapshotNodeResponseRT.encode(snapshotResponse), diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index 21420095a3ae5..0fef75faed07e 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -53,21 +53,25 @@ export const getNodes = async ( client: ESSearchClient, snapshotRequest: SnapshotRequest, source: InfraSource, - logQueryFields: LogQueryFields, - compositeSize: number + compositeSize: number, + logQueryFields?: LogQueryFields ) => { let nodes; if (snapshotRequest.metrics.find((metric) => metric.type === 'logRate')) { // *Only* the log rate metric has been requested if (snapshotRequest.metrics.length === 1) { - nodes = await transformAndQueryData({ - client, - snapshotRequest, - source, - compositeSize, - sourceOverrides: logQueryFields, - }); + if (logQueryFields != null) { + nodes = await transformAndQueryData({ + client, + snapshotRequest, + source, + compositeSize, + sourceOverrides: logQueryFields, + }); + } else { + nodes = { nodes: [], interval: '60s' }; + } } else { // A scenario whereby a single host might be shipping metrics and logs. const metricsWithoutLogsMetrics = snapshotRequest.metrics.filter( @@ -79,13 +83,16 @@ export const getNodes = async ( source, compositeSize, }); - const logRateNodes = await transformAndQueryData({ - client, - snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] }, - source, - compositeSize, - sourceOverrides: logQueryFields, - }); + const logRateNodes = + logQueryFields != null + ? await transformAndQueryData({ + client, + snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] }, + source, + compositeSize, + sourceOverrides: logQueryFields, + }) + : { nodes: [], interval: '60s' }; // Merge nodes where possible - e.g. a single host is shipping metrics and logs const mergedNodes = nodesWithoutLogsMetrics.nodes.map((node) => { const logRateNode = logRateNodes.nodes.find( diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx index 8f6f9329f2974..703c620a4904c 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx @@ -704,4 +704,43 @@ describe('BuilderEntryItem', () => { expect(mockSetErrorExists).toHaveBeenCalledWith(true); }); + + test('it disabled field inputs correctly when passed "isDisabled=true"', () => { + wrapper = mount( + + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryField"] input').props().disabled + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"] input').props().disabled + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"] input').props().disabled + ).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index e13a7ccf90bdd..f26d10a16c877 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; @@ -22,6 +22,7 @@ import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_ import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists'; import { ExceptionListType, ListSchema, OperatorTypeEnum } from '../../../../common'; import { getEmptyValue } from '../../../common/empty_value'; +import { OsTypeArray } from '../../../../common/schemas/common'; import { getEntryOnFieldChange, @@ -45,15 +46,18 @@ export interface EntryItemProps { entry: FormattedBuilderEntry; httpService: HttpStart; indexPattern: IIndexPattern; + showLabel: boolean; + osTypes?: OsTypeArray; listType: ExceptionListType; listTypeSpecificIndexPatternFilter?: ( pattern: IIndexPattern, - type: ExceptionListType + type: ExceptionListType, + osTypes?: OsTypeArray ) => IIndexPattern; onChange: (arg: BuilderEntry, i: number) => void; onlyShowListOperators?: boolean; setErrorsExist: (arg: boolean) => void; - showLabel: boolean; + isDisabled?: boolean; } export const BuilderEntryItem: React.FC = ({ @@ -62,12 +66,14 @@ export const BuilderEntryItem: React.FC = ({ entry, httpService, indexPattern, + osTypes, listType, listTypeSpecificIndexPatternFilter, onChange, onlyShowListOperators = false, setErrorsExist, showLabel, + isDisabled = false, }): JSX.Element => { const handleError = useCallback( (err: boolean): void => { @@ -120,13 +126,22 @@ export const BuilderEntryItem: React.FC = ({ [onChange, entry] ); + const isFieldComponentDisabled = useMemo( + (): boolean => + isDisabled || + indexPattern == null || + (indexPattern != null && indexPattern.fields.length === 0), + [isDisabled, indexPattern] + ); + const renderFieldInput = useCallback( (isFirst: boolean): JSX.Element => { const filteredIndexPatterns = getFilteredIndexPatterns( indexPattern, entry, listType, - listTypeSpecificIndexPatternFilter + listTypeSpecificIndexPatternFilter, + osTypes ); const comboBox = ( = ({ selectedField={entry.field} isClearable={false} isLoading={false} - isDisabled={indexPattern == null} + isDisabled={isDisabled || indexPattern == null} onChange={handleFieldChange} data-test-subj="exceptionBuilderEntryField" fieldInputWidth={275} @@ -160,7 +175,15 @@ export const BuilderEntryItem: React.FC = ({ ); } }, - [indexPattern, entry, listType, listTypeSpecificIndexPatternFilter, handleFieldChange] + [ + indexPattern, + entry, + listType, + listTypeSpecificIndexPatternFilter, + handleFieldChange, + osTypes, + isDisabled, + ] ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { @@ -177,9 +200,7 @@ export const BuilderEntryItem: React.FC = ({ placeholder={i18n.EXCEPTION_OPERATOR_PLACEHOLDER} selectedField={entry.field} operator={entry.operator} - isDisabled={ - indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0) - } + isDisabled={isFieldComponentDisabled} operatorOptions={operatorOptions} isLoading={false} isClearable={false} @@ -214,9 +235,7 @@ export const BuilderEntryItem: React.FC = ({ placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER} selectedField={entry.correspondingKeywordField ?? entry.field} selectedValue={value} - isDisabled={ - indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0) - } + isDisabled={isFieldComponentDisabled} isLoading={false} isClearable={false} indexPattern={indexPattern} @@ -239,9 +258,7 @@ export const BuilderEntryItem: React.FC = ({ : entry.field } selectedValue={values} - isDisabled={ - indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0) - } + isDisabled={isFieldComponentDisabled} isLoading={false} isClearable={false} indexPattern={indexPattern} @@ -261,9 +278,7 @@ export const BuilderEntryItem: React.FC = ({ placeholder={i18n.EXCEPTION_FIELD_LISTS_PLACEHOLDER} selectedValue={id} isLoading={false} - isDisabled={ - indexPattern == null || (indexPattern != null && indexPattern.fields.length === 0) - } + isDisabled={isFieldComponentDisabled} isClearable={false} onChange={handleFieldListValueChange} data-test-subj="exceptionBuilderEntryFieldList" diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index c9cbd9a84f5e3..11e64630b242d 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -13,6 +13,7 @@ import { AutocompleteStart } from 'src/plugins/data/public'; import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { OsTypeArray } from '../../../../common/schemas'; import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; import { BuilderAndBadgeComponent } from './and_badge'; @@ -41,18 +42,21 @@ interface BuilderExceptionListItemProps { autocompleteService: AutocompleteStart; exceptionItem: ExceptionsBuilderExceptionItem; exceptionItemIndex: number; + osTypes?: OsTypeArray; indexPattern: IIndexPattern; andLogicIncluded: boolean; isOnlyItem: boolean; listType: ExceptionListType; listTypeSpecificIndexPatternFilter?: ( pattern: IIndexPattern, - type: ExceptionListType + type: ExceptionListType, + osTypes?: OsTypeArray ) => IIndexPattern; onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; setErrorsExist: (arg: boolean) => void; onlyShowListOperators?: boolean; + isDisabled?: boolean; } export const BuilderExceptionListItemComponent = React.memo( @@ -61,6 +65,7 @@ export const BuilderExceptionListItemComponent = React.memo { const handleEntryChange = useCallback( (entry: BuilderEntry, entryIndex: number): void => { @@ -138,6 +144,8 @@ export const BuilderExceptionListItemComponent = React.memo IIndexPattern; onChange: (arg: OnChangeProps) => void; ruleName: string; + isDisabled?: boolean; } export const ExceptionBuilderComponent = ({ @@ -102,6 +105,8 @@ export const ExceptionBuilderComponent = ({ listTypeSpecificIndexPatternFilter, onChange, ruleName, + isDisabled = false, + osTypes, }: ExceptionBuilderProps): JSX.Element => { const [ { @@ -187,7 +192,6 @@ export const ExceptionBuilderComponent = ({ (shouldAddNested: boolean): void => { dispatch({ addNested: shouldAddNested, - type: 'setAddNested', }); }, @@ -342,6 +346,10 @@ export const ExceptionBuilderComponent = ({ }); }, [onChange, exceptionsToDelete, exceptions, errorExists]); + useEffect(() => { + setUpdateExceptions([]); + }, [osTypes, setUpdateExceptions]); + // Defaults builder to never be sans entry, instead // always falls back to an empty entry if user deletes all useEffect(() => { @@ -401,6 +409,8 @@ export const ExceptionBuilderComponent = ({ onDeleteExceptionItem={handleDeleteExceptionItem} onlyShowListOperators={containsValueListEntry(exceptions)} setErrorsExist={setErrorsExist} + osTypes={osTypes} + isDisabled={isDisabled} />
@@ -417,8 +427,8 @@ export const ExceptionBuilderComponent = ({ IIndexPattern + preFilter?: (i: IIndexPattern, t: ExceptionListType, o?: OsTypeArray) => IIndexPattern, + osTypes?: OsTypeArray ): IIndexPattern => { - const indexPatterns = preFilter != null ? preFilter(patterns, type) : patterns; + const indexPatterns = preFilter != null ? preFilter(patterns, type, osTypes) : patterns; if (item.nested === 'child' && item.parent != null) { // when user has selected a nested entry, only fields with the common parent are shown diff --git a/x-pack/plugins/metrics_entities/README.md b/x-pack/plugins/metrics_entities/README.md new file mode 100755 index 0000000000000..6c711ce4fed82 --- /dev/null +++ b/x-pack/plugins/metrics_entities/README.md @@ -0,0 +1,324 @@ +# metrics_entities + +This is the metrics and entities plugin where you add can add transforms for your project +and group those transforms into modules. You can also re-use existing transforms in your +modules as well. + +## Turn on experimental flags +During at least phase 1 of this development, please add these to your `kibana.dev.yml` file to turn on the feature: + +```ts +xpack.metricsEntities.enabled: true +xpack.securitySolution.enableExperimental: ['metricsEntitiesEnabled'] +``` + +## Quick start on using scripts to call the API + +The scripts rely on CURL and jq: + +- [CURL](https://curl.haxx.se) +- [jq](https://stedolan.github.io/jq/) + +Install curl and jq + +```sh +brew update +brew install curl +brew install jq +``` + +Open `$HOME/.zshrc` or `${HOME}.bashrc` depending on your SHELL output from `echo $SHELL` +and add these environment variables: + +```sh +export ELASTICSEARCH_USERNAME=${user} +export ELASTICSEARCH_PASSWORD=${password} +export ELASTICSEARCH_URL=https://${ip}:9200 +export KIBANA_URL=http://localhost:5601 +``` + +source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: + +```sh +source ~/.zshrc +``` + +Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will +get in the way of the CURL scripts written as is. + +Go to the scripts folder `cd kibana/x-pack/plugins/metrics_entities/server/scripts` and can run some of the scripts +such as: + +```sh +./post_transforms.sh ./post_examples/all.json +``` + +which will post transforms from the `all.json` + +You can also delete them by running: + +```sh +./delete_transforms.sh ./delete_examples/all.json +``` + +See the folder for other curl scripts that exercise parts of the REST API and feel free to add your own examples +in the folder as well. + +## Quick start on how to add a transform + +You will want to figure out how you want your transform from within Kibana roughly using +the UI and then copy the JSON. The JSON you will want to change and paste within a folder +which represents a module. + +For example, for the `host_entities` and a `host_entities_mapping` we created a folder called host_entities +here: + +```sh +sever/modules/host_entities +``` + +Then we add two files, a subset of the transform JSON and a mapping like so: + +```sh +server/modules/host_entities/host_entities_mapping.json <--- this is the mappings +server/modules/host_entities/host_entities.json <--- This is a subset of the transform JSON +index.ts <--- Import/export your json here +``` + +The mappings can be normal mapping like so with `host_entities_mapping.json`: +```json +{ + "mappings": { + "_meta": { + "index": "host_ent" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "host": { + "properties": { + "name": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + }, + "host": { + "properties": { + "name": { + "type": "keyword" + }, + "os": { + "properties": { + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + } + } + } + } + } +} +``` + +One caveat is that you need to add this to the meta section to tell it what the name will be: +```json + "_meta": { + "index": "host_ent" + }, +``` + +Keep the name short as there is only 65 characters for a transform job and we prepend extra information to the mapping such as: +* prefix +* name of estc + +Although not required, a `"dynamic": "strict"` is strongly encouraged to prevent mapping guesses from elastic and it will be better for us +to spot errors quicker in the mappings such as type-o's if this is set to strict. + +Next, for the transform, you should add a subset that doesn't have any additional settings or meta associated like so for `host_entities.json`: + +```json +{ + "id": "host_ent", + "description": "[host.name entities] grouped by @timestamp, host.name, os.name, and os.version, and aggregated on host.name", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + }, + "host.name": { + "terms": { + "field": "host.name" + } + }, + "host.os.name": { + "terms": { + "field": "host.os.name", + "missing_bucket": true + } + }, + "host.os.version": { + "terms": { + "field": "host.os.version", + "missing_bucket": true + } + } + }, + "aggregations": { + "metrics.host.name.value_count": { + "value_count": { + "field": "host.name" + } + } + } + } +} +``` + +Look in the `server/modules` for other examples, but it should be that clear cut. The final part is to wire everything up in the code by touching a few files +to either add this to an existing module or create your own module. In `server/module/host_entities` we add an `index.ts` like so that does an import/export +of the JSON: + +```sh +import hostEntities from './host_entities.json'; +import hostEntitiesMapping from './host_entities_mapping.json'; +export { hostEntities, hostEntitiesMapping }; +``` + +Then in `modules/index.ts` we add a new module name if we are creating a new module to the `export enum ModuleNames {` like so: + +```ts +// Import your host entities you just made +import { hostEntities, hostEntitiesMapping } from './host_entities'; + +/** + * These module names will map 1 to 1 to the REST interface. + */ +export enum ModuleNames { + hostSummaryMetrics = 'host_metrics', + hostSummaryEntities = 'host_entities', // <-- Add the entities/transform and give it a enum name and a module name + networkSummaryEntities = 'network_entities', + networkSummaryMetrics = 'network_metrics', + userSummaryEntities = 'user_entities', + userSummaryMetrics = 'user_metrics', +} +``` + +If you're not creating a new module but rather you are adding to an existing module, you can skip the above step. Next, you +just need to add your installable transform and installable mapping to the two data structures of `installableTransforms` and +`installableMappings` like so: + +```ts +/** + * Add any new folders as modules with their names below and grouped with + * key values. + */ +export const installableTransforms: Record = { + [ModuleNames.hostSummaryMetrics]: [hostMetrics], + [ModuleNames.hostSummaryEntities]: [hostEntities], // <-- Adds my new module name and transform to a new array. + [ModuleNames.networkSummaryEntities]: [ + destinationIpEntities, // <-- If instead I am adding to an existing module, I just add it to the array like these show + sourceIpEntities, + destinationCountryIsoCodeEntities, + sourceCountryIsoCodeEntities, + ], + [ModuleNames.networkSummaryMetrics]: [ipMetrics], + [ModuleNames.userSummaryEntities]: [userEntities], + [ModuleNames.userSummaryMetrics]: [userMetrics], +}; + +/** + * For all the mapping types, add each with their names below and grouped with + * key values. + */ +export const installableMappings: Record = { + [ModuleNames.hostSummaryMetrics]: [hostMetricsMapping], + [ModuleNames.hostSummaryEntities]: [hostEntitiesMapping], // <-- Adds my new module name and mapping to a new array. + [ModuleNames.networkSummaryEntities]: [ // <-- If instead I am adding to an existing module, I just add it to the array like these show + sourceIpEntitiesMapping, + destinationIpEntitiesMapping, + destinationCountryIsoCodeEntitiesMapping, + sourceCountryIsoCodeEntitiesMapping, + ], + [ModuleNames.networkSummaryMetrics]: [ipMetricsMapping], + [ModuleNames.userSummaryEntities]: [userEntitiesMapping], + [ModuleNames.userSummaryMetrics]: [userMetricsMapping], +}; +``` + +And after that, you should check out if there are any existing e2e tests or unit tests to update here to ensure that your mapping and transform will +pass ci. Create a pull request and your mapping and transform are completed. + +To call into the code to activate your module and create your transforms and mappings would be the following where you substitute your +${KIBANA_URL} with your kibana URL and the ${SPACE_URL} with any space id you have. If you're using the default space then you would use +an empty string: +```json +POST ${KIBANA_URL}${SPACE_URL}/api/metrics_entities/transforms +{ + "prefix": "all", + "modules": [ + "host_entities", + ], + "indices": [ + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + "-*elastic-cloud-logs-*" + ], + "auto_start": true, + "settings": { + "max_page_search_size": 5000 + }, + "query": { + "range": { + "@timestamp": { + "gte": "now-1d/d", + "format": "strict_date_optional_time" + } + } + } +} +``` + +Very similar to the regular transform REST API, with the caveats that you define which modules you want to install, the prefix name you want to use, and +if you want to `auto_start` it or not. The rest such as `settings`, `query` will be the same as the transforms API. They will also push those same setting into +each of your transforms within your module(s) as the same setting for each individual ones. + +## TODO List +During the phase 1, phase 2, phase N, this TODO will exist as a reminder and notes for what still needs to be developed. These are not in a priority order, but +are notes during the phased approach. As we approach production and the feature flags are removed these TODO's should be removed in favor of Kibana issues or regular +left over TODO's in the code base. + +- Add these properties to the route which are: + - disable_transforms/exclude flag to exclude 1 or more transforms within a module, + - pipeline flag, + - Change the REST routes on post to change the indexes for whichever indexes you want + - Unit tests to ensure the data of the mapping.json includes the correct fields such as + _meta, at least one alias, a mapping section, etc... + - Add text/keyword and other things to the mappings (not just keyword maybe?) ... At least review the mappings one more time + - Add a sort of @timestamp to the output destination indexes? + - Add the REST Kibana security based tags if needed and push those to any plugins using this plugin. Something like: tags: ['access:metricsEntities-read'] and ['access:metricsEntities-all'], + - Add schema validation choosing some schema library (io-ts or Kibana Schema or ... ) + - Add unit tests + - Add e2e tests + - Move ui code into this plugin from security_solutions? (maybe?) + - UI code could be within `kibana/packages` instead of in here directly and I think we will be better off. diff --git a/x-pack/plugins/metrics_entities/common/constants.ts b/x-pack/plugins/metrics_entities/common/constants.ts new file mode 100644 index 0000000000000..8efa0327f5f41 --- /dev/null +++ b/x-pack/plugins/metrics_entities/common/constants.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Base route + */ +export const METRICS_ENTITIES_URL = '/api/metrics_entities'; + +/** + * Transforms route + */ +export const METRICS_ENTITIES_TRANSFORMS = `${METRICS_ENTITIES_URL}/transforms`; + +/** + * Global prefix for all the transform jobs + */ +export const ELASTIC_NAME = 'estc'; diff --git a/x-pack/plugins/metrics_entities/common/index.ts b/x-pack/plugins/metrics_entities/common/index.ts new file mode 100644 index 0000000000000..a532dc151bf46 --- /dev/null +++ b/x-pack/plugins/metrics_entities/common/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const PLUGIN_ID = 'metricsEntities'; +export const PLUGIN_NAME = 'metrics_entities'; + +export * from './constants'; diff --git a/x-pack/plugins/metrics_entities/jest.config.js b/x-pack/plugins/metrics_entities/jest.config.js new file mode 100644 index 0000000000000..402532aa44c41 --- /dev/null +++ b/x-pack/plugins/metrics_entities/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/metrics_entities'], +}; diff --git a/x-pack/plugins/metrics_entities/kibana.json b/x-pack/plugins/metrics_entities/kibana.json new file mode 100644 index 0000000000000..17484c2c243ce --- /dev/null +++ b/x-pack/plugins/metrics_entities/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "metricsEntities", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "metricsEntities"], + "server": true, + "ui": false, + "requiredPlugins": ["data", "dataEnhanced"], + "optionalPlugins": [] +} diff --git a/x-pack/plugins/metrics_entities/server/config.ts b/x-pack/plugins/metrics_entities/server/config.ts new file mode 100644 index 0000000000000..31be256611803 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/config.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/metrics_entities/server/error_with_status_code.ts b/x-pack/plugins/metrics_entities/server/error_with_status_code.ts new file mode 100644 index 0000000000000..15f7797fa424f --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/error_with_status_code.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class ErrorWithStatusCode extends Error { + private readonly statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } + + public getStatusCode = (): number => this.statusCode; +} diff --git a/x-pack/plugins/metrics_entities/server/index.ts b/x-pack/plugins/metrics_entities/server/index.ts new file mode 100644 index 0000000000000..b4d35eb90f486 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { PluginInitializerContext } from '../../../../src/core/server'; + +import { ConfigSchema } from './config'; +import { MetricsEntitiesPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext): MetricsEntitiesPlugin => { + return new MetricsEntitiesPlugin(initializerContext); +}; + +export { MetricsEntitiesPluginSetup, MetricsEntitiesPluginStart } from './types'; diff --git a/x-pack/plugins/metrics_entities/server/modules/README.md b/x-pack/plugins/metrics_entities/server/modules/README.md new file mode 100644 index 0000000000000..d4e28a2f83ed0 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/README.md @@ -0,0 +1,4 @@ +# Modules + +This is where all the module types exist so you can load different bundled modules +with a REST endpoint. \ No newline at end of file diff --git a/x-pack/plugins/metrics_entities/server/modules/host_entities/host_entities.json b/x-pack/plugins/metrics_entities/server/modules/host_entities/host_entities.json new file mode 100644 index 0000000000000..ef6bcfc452860 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/host_entities/host_entities.json @@ -0,0 +1,38 @@ +{ + "id": "host_ent", + "description": "[host.name entities] grouped by @timestamp, host.name, os.name, and os.version, and aggregated on host.name", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + }, + "host.name": { + "terms": { + "field": "host.name" + } + }, + "host.os.name": { + "terms": { + "field": "host.os.name", + "missing_bucket": true + } + }, + "host.os.version": { + "terms": { + "field": "host.os.version", + "missing_bucket": true + } + } + }, + "aggregations": { + "metrics.host.name.value_count": { + "value_count": { + "field": "host.name" + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/host_entities/host_entities_mapping.json b/x-pack/plugins/metrics_entities/server/modules/host_entities/host_entities_mapping.json new file mode 100644 index 0000000000000..1f1e93dabfb5f --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/host_entities/host_entities_mapping.json @@ -0,0 +1,45 @@ +{ + "mappings": { + "_meta": { + "index": "host_ent" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "host": { + "properties": { + "name": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + }, + "host": { + "properties": { + "name": { + "type": "keyword" + }, + "os": { + "properties": { + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/host_entities/index.ts b/x-pack/plugins/metrics_entities/server/modules/host_entities/index.ts new file mode 100644 index 0000000000000..c3f34cd0f535c --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/host_entities/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import hostEntities from './host_entities.json'; +import hostEntitiesMapping from './host_entities_mapping.json'; +export { hostEntities, hostEntitiesMapping }; diff --git a/x-pack/plugins/metrics_entities/server/modules/host_metrics/host_metrics.json b/x-pack/plugins/metrics_entities/server/modules/host_metrics/host_metrics.json new file mode 100644 index 0000000000000..8388721f98926 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/host_metrics/host_metrics.json @@ -0,0 +1,21 @@ +{ + "id": "host_met", + "description": "[host.name metrics] grouped by @timestamp and aggregated on host.name", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + } + }, + "aggregations": { + "metrics.host.name.cardinality": { + "cardinality": { + "field": "host.name" + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/host_metrics/host_metrics_mapping.json b/x-pack/plugins/metrics_entities/server/modules/host_metrics/host_metrics_mapping.json new file mode 100644 index 0000000000000..7975fe3c6ed0a --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/host_metrics/host_metrics_mapping.json @@ -0,0 +1,83 @@ +{ + "mappings": { + "_meta": { + "index": "host_met" + }, + "properties": { + "metrics": { + "properties": { + "source": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "community_id": { + "properties": { + "cardinality": { + "type": "long" + } + } + } + } + }, + "host": { + "properties": { + "name": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + } + } + } + } + }, + "@timestamp": { + "type": "date" + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/host_metrics/index.ts b/x-pack/plugins/metrics_entities/server/modules/host_metrics/index.ts new file mode 100644 index 0000000000000..e11c5321ede85 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/host_metrics/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import hostMetrics from './host_metrics.json'; +import hostMetricsMapping from './host_metrics_mapping.json'; + +export { hostMetrics, hostMetricsMapping }; diff --git a/x-pack/plugins/metrics_entities/server/modules/index.ts b/x-pack/plugins/metrics_entities/server/modules/index.ts new file mode 100644 index 0000000000000..61aca783a6c03 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/index.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { hostMetrics, hostMetricsMapping } from './host_metrics'; +import { userMetrics, userMetricsMapping } from './user_metrics'; +import { ipMetrics, ipMetricsMapping } from './network_metrics'; +import { hostEntities, hostEntitiesMapping } from './host_entities'; +import { + destinationCountryIsoCodeEntities, + destinationCountryIsoCodeEntitiesMapping, + destinationIpEntities, + destinationIpEntitiesMapping, + sourceCountryIsoCodeEntities, + sourceCountryIsoCodeEntitiesMapping, + sourceIpEntities, + sourceIpEntitiesMapping, +} from './network_entities'; +import { Mappings, Transforms } from './types'; +import { userEntities, userEntitiesMapping } from './user_entities'; + +/** + * These module names will map 1 to 1 to the REST interface. + */ +export enum ModuleNames { + hostSummaryMetrics = 'host_metrics', + hostSummaryEntities = 'host_entities', + networkSummaryEntities = 'network_entities', + networkSummaryMetrics = 'network_metrics', + userSummaryEntities = 'user_entities', + userSummaryMetrics = 'user_metrics', +} + +/** + * Add any new folders as modules with their names below and grouped with + * key values. + */ +export const installableTransforms: Record = { + [ModuleNames.hostSummaryMetrics]: [hostMetrics], + [ModuleNames.hostSummaryEntities]: [hostEntities], + [ModuleNames.networkSummaryEntities]: [ + destinationIpEntities, + sourceIpEntities, + destinationCountryIsoCodeEntities, + sourceCountryIsoCodeEntities, + ], + [ModuleNames.networkSummaryMetrics]: [ipMetrics], + [ModuleNames.userSummaryEntities]: [userEntities], + [ModuleNames.userSummaryMetrics]: [userMetrics], +}; + +/** + * For all the mapping types, add each with their names below and grouped with + * key values. + */ +export const installableMappings: Record = { + [ModuleNames.hostSummaryMetrics]: [hostMetricsMapping], + [ModuleNames.hostSummaryEntities]: [hostEntitiesMapping], + [ModuleNames.networkSummaryEntities]: [ + sourceIpEntitiesMapping, + destinationIpEntitiesMapping, + destinationCountryIsoCodeEntitiesMapping, + sourceCountryIsoCodeEntitiesMapping, + ], + [ModuleNames.networkSummaryMetrics]: [ipMetricsMapping], + [ModuleNames.userSummaryEntities]: [userEntitiesMapping], + [ModuleNames.userSummaryMetrics]: [userMetricsMapping], +}; diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_country_iso_code_entities.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_country_iso_code_entities.json new file mode 100644 index 0000000000000..1f39c6c9552bd --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_country_iso_code_entities.json @@ -0,0 +1,51 @@ +{ + "id": "dest_iso_ent", + "description": "[destination.geo.country_iso_code entities] grouped by @timestamp and aggregated on source.bytes, destination.bytes, network.community_id, destination.ip, and source.ip", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + }, + "destination.geo.country_iso_code": { + "terms": { + "field": "destination.geo.country_iso_code" + } + } + }, + "aggregations": { + "metrics.destination.geo.country_iso_code.value_count": { + "value_count": { + "field": "destination.geo.country_iso_code" + } + }, + "metrics.source.bytes.sum": { + "sum": { + "field": "source.bytes" + } + }, + "metrics.destination.bytes.sum": { + "sum": { + "field": "destination.bytes" + } + }, + "metrics.network.community_id.cardinality": { + "cardinality": { + "field": "network.community_id" + } + }, + "metrics.source.ip.cardinality": { + "cardinality": { + "field": "source.ip" + } + }, + "metrics.destination.ip.cardinality": { + "cardinality": { + "field": "destination.ip" + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_country_iso_code_entities_mapping.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_country_iso_code_entities_mapping.json new file mode 100644 index 0000000000000..e56ed7157afdc --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_country_iso_code_entities_mapping.json @@ -0,0 +1,120 @@ +{ + "mappings": { + "_meta": { + "index": "dest_iso_ent" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "source": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + }, + "geo": { + "properties": { + "country_iso_code": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + }, + "geo": { + "properties": { + "country_iso_code": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + }, + "network": { + "properties": { + "community_id": { + "properties": { + "cardinality": { + "type": "long" + } + } + } + } + } + } + }, + "source": { + "properties": { + "ip": { + "type": "ip" + }, + "geo": { + "properties": { + "country_iso_code": { + "type": "keyword" + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "type": "ip" + }, + "geo": { + "properties": { + "country_iso_code": { + "type": "keyword" + } + } + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_ip_entities.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_ip_entities.json new file mode 100644 index 0000000000000..7ecced9a11ebc --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_ip_entities.json @@ -0,0 +1,46 @@ +{ + "id": "dest_ip_ent", + "description": "[destination.ip entities] grouped by @timestamp and aggregated on destination.ip, source.bytes, destination.bytes, network.community_id, and source.ip", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + }, + "destination.ip": { + "terms": { + "field": "destination.ip" + } + } + }, + "aggregations": { + "metrics.destination.ip.value_count": { + "value_count": { + "field": "destination.ip" + } + }, + "metrics.source.bytes.sum": { + "sum": { + "field": "source.bytes" + } + }, + "metrics.destination.bytes.sum": { + "sum": { + "field": "destination.bytes" + } + }, + "metrics.network.community_id.cardinality": { + "cardinality": { + "field": "network.community_id" + } + }, + "metrics.source.ip.cardinality": { + "cardinality": { + "field": "source.ip" + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_ip_entities_mapping.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_ip_entities_mapping.json new file mode 100644 index 0000000000000..ef7e1050c9c5d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/destination_ip_entities_mapping.json @@ -0,0 +1,84 @@ +{ + "mappings": { + "_meta": { + "index": "dest_ip_ent" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "source": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "community_id": { + "properties": { + "cardinality": { + "type": "long" + } + } + } + } + } + } + }, + "source": { + "properties": { + "ip": { + "type": "ip" + } + } + }, + "destination": { + "properties": { + "ip": { + "type": "ip" + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/index.ts b/x-pack/plugins/metrics_entities/server/modules/network_entities/index.ts new file mode 100644 index 0000000000000..b54425763effb --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/index.ts @@ -0,0 +1,26 @@ +/* + * 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 sourceIpEntities from './source_ip_entities.json'; +import destinationIpEntities from './destination_ip_entities.json'; +import sourceIpEntitiesMapping from './source_ip_entities_mapping.json'; +import destinationIpEntitiesMapping from './destination_ip_entities_mapping.json'; +import destinationCountryIsoCodeEntities from './destination_country_iso_code_entities.json'; +import destinationCountryIsoCodeEntitiesMapping from './destination_country_iso_code_entities_mapping.json'; +import sourceCountryIsoCodeEntities from './source_country_iso_code_entities.json'; +import sourceCountryIsoCodeEntitiesMapping from './source_country_iso_code_entities_mapping.json'; + +export { + sourceIpEntities, + destinationIpEntities, + sourceCountryIsoCodeEntities, + sourceCountryIsoCodeEntitiesMapping, + destinationCountryIsoCodeEntities, + destinationCountryIsoCodeEntitiesMapping, + sourceIpEntitiesMapping, + destinationIpEntitiesMapping, +}; diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/source_country_iso_code_entities.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_country_iso_code_entities.json new file mode 100644 index 0000000000000..60021b975b21d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_country_iso_code_entities.json @@ -0,0 +1,51 @@ +{ + "id": "src_iso_ent", + "description": "[source.geo.country_iso_code entities] grouped by @timestamp and aggregated on source.geo.country_iso_code, source.bytes, destination.bytes, network.community_id, source.ip, and destination.ip", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + }, + "source.geo.country_iso_code": { + "terms": { + "field": "source.geo.country_iso_code" + } + } + }, + "aggregations": { + "metrics.source.geo.country_iso_code.value_count": { + "value_count": { + "field": "source.geo.country_iso_code" + } + }, + "metrics.source.bytes.sum": { + "sum": { + "field": "source.bytes" + } + }, + "metrics.destination.bytes.sum": { + "sum": { + "field": "destination.bytes" + } + }, + "metrics.network.community_id.cardinality": { + "cardinality": { + "field": "network.community_id" + } + }, + "metrics.source.ip.cardinality": { + "cardinality": { + "field": "source.ip" + } + }, + "metrics.destination.ip.cardinality": { + "cardinality": { + "field": "destination.ip" + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/source_country_iso_code_entities_mapping.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_country_iso_code_entities_mapping.json new file mode 100644 index 0000000000000..0a44016be6a2c --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_country_iso_code_entities_mapping.json @@ -0,0 +1,120 @@ +{ + "mappings": { + "_meta": { + "index": "src_iso_ent" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "source": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + }, + "geo": { + "properties": { + "country_iso_code": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + }, + "geo": { + "properties": { + "country_iso_code": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + }, + "network": { + "properties": { + "community_id": { + "properties": { + "cardinality": { + "type": "long" + } + } + } + } + } + } + }, + "source": { + "properties": { + "ip": { + "type": "ip" + }, + "geo": { + "properties": { + "country_iso_code": { + "type": "keyword" + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "type": "ip" + }, + "geo": { + "properties": { + "country_iso_code": { + "type": "keyword" + } + } + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/source_ip_entities.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_ip_entities.json new file mode 100644 index 0000000000000..3de6669c7bedb --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_ip_entities.json @@ -0,0 +1,46 @@ +{ + "id": "src_ip_ent", + "description": "[source.ip entities] grouped by @timestamp and aggregated on destination.ip, source.bytes, destination.bytes, network.community_id, and destination.ip", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + }, + "source.ip": { + "terms": { + "field": "source.ip" + } + } + }, + "aggregations": { + "metrics.source.ip.value_count": { + "value_count": { + "field": "source.ip" + } + }, + "metrics.source.bytes.sum": { + "sum": { + "field": "source.bytes" + } + }, + "metrics.destination.bytes.sum": { + "sum": { + "field": "destination.bytes" + } + }, + "metrics.network.community_id.cardinality": { + "cardinality": { + "field": "network.community_id" + } + }, + "metrics.destination.ip.cardinality": { + "cardinality": { + "field": "destination.ip" + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_entities/source_ip_entities_mapping.json b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_ip_entities_mapping.json new file mode 100644 index 0000000000000..64d9e48afcee9 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_entities/source_ip_entities_mapping.json @@ -0,0 +1,84 @@ +{ + "mappings": { + "_meta": { + "index": "src_ip_ent" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "source": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "community_id": { + "properties": { + "cardinality": { + "type": "long" + } + } + } + } + } + } + }, + "source": { + "properties": { + "ip": { + "type": "ip" + } + } + }, + "destination": { + "properties": { + "ip": { + "type": "ip" + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_metrics/index.ts b/x-pack/plugins/metrics_entities/server/modules/network_metrics/index.ts new file mode 100644 index 0000000000000..216b85234dda4 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_metrics/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import ipMetrics from './ip_metrics.json'; +import ipMetricsMapping from './ip_metrics_mapping.json'; + +export { ipMetrics, ipMetricsMapping }; diff --git a/x-pack/plugins/metrics_entities/server/modules/network_metrics/ip_metrics.json b/x-pack/plugins/metrics_entities/server/modules/network_metrics/ip_metrics.json new file mode 100644 index 0000000000000..ed953be84f3da --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_metrics/ip_metrics.json @@ -0,0 +1,116 @@ +{ + "id": "ip_met", + "description": "[source.ip metrics] grouped by @timestamp, source.ip, destination.ip and aggregated on tls.version, suricata.eve.tls.version, zeek.ssl.version, dns.question.name, and zeek.dns.query", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + } + }, + "aggregations": { + "metrics.source.ip.cardinality": { + "cardinality": { + "field": "source.ip" + } + }, + "metrics.destination.ip.cardinality": { + "cardinality": { + "field": "destination.ip" + } + }, + "metrics.network": { + "filter": { + "bool": { + "should": [ + { + "exists": { + "field": "source.ip" + } + }, + { + "exists": { + "field": "destination.ip" + } + } + ], + "minimum_should_match": 1 + } + }, + "aggs": { + "events.value_count": { + "value_count": { + "field": "@timestamp" + } + }, + "tls": { + "filter": { + "bool": { + "should": [ + { + "exists": { + "field": "tls.version" + } + }, + { + "exists": { + "field": "suricata.eve.tls.version" + } + }, + { + "exists": { + "field": "zeek.ssl.version" + } + } + ], + "minimum_should_match": 1 + } + }, + "aggs": { + "version.value_count": { + "value_count": { + "field": "@timestamp" + } + } + } + } + } + }, + "metrics.dns": { + "filter": { + "bool": { + "should": [ + { + "exists": { + "field": "dns.question.name" + } + }, + { + "term": { + "suricata.eve.dns.type": { + "value": "query" + } + } + }, + { + "exists": { + "field": "zeek.dns.query" + } + } + ], + "minimum_should_match": 1 + } + }, + "aggs": { + "queries.value_count": { + "value_count": { + "field": "@timestamp" + } + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/network_metrics/ip_metrics_mapping.json b/x-pack/plugins/metrics_entities/server/modules/network_metrics/ip_metrics_mapping.json new file mode 100644 index 0000000000000..a855b6091f29c --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/network_metrics/ip_metrics_mapping.json @@ -0,0 +1,92 @@ +{ + "mappings": { + "_meta": { + "index": "ip_met" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "source": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "destination": { + "properties": { + "ip": { + "properties": { + "value_count": { + "type": "long" + }, + "cardinality": { + "type": "long" + } + } + }, + "bytes": { + "properties": { + "sum": { + "type": "long" + } + } + } + } + }, + "network": { + "properties": { + "events": { + "properties": { + "value_count": { + "type": "long" + } + } + }, + "tls": { + "properties": { + "version": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + }, + "dns": { + "properties": { + "queries": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/types.ts b/x-pack/plugins/metrics_entities/server/modules/types.ts new file mode 100644 index 0000000000000..22b11ed89f5c4 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/types.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Loose type for the mappings + */ +export interface Mappings { + [key: string]: unknown; + mappings: { + [key: string]: unknown; + _meta: { + index: string; + }; + }; +} + +/** + * Loose type for the transforms. id is marked optional so we can delete it before + * pushing it through elastic client. + * TODO: Can we use stricter pre-defined typings for the transforms here or is this ours because we define it slightly different? + */ +export interface Transforms { + [key: string]: unknown; + id: string; + dest?: Partial<{ + index: string; + pipeline: string; + }>; + source?: Partial<{}>; + settings?: Partial<{ + max_page_search_size: number; + docs_per_second: number | null; + }>; +} diff --git a/x-pack/plugins/metrics_entities/server/modules/user_entities/index.ts b/x-pack/plugins/metrics_entities/server/modules/user_entities/index.ts new file mode 100644 index 0000000000000..9cc17c8f180f0 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/user_entities/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import userEntities from './user_entities.json'; +import userEntitiesMapping from './user_entities_mapping.json'; +export { userEntities, userEntitiesMapping }; diff --git a/x-pack/plugins/metrics_entities/server/modules/user_entities/user_entities.json b/x-pack/plugins/metrics_entities/server/modules/user_entities/user_entities.json new file mode 100644 index 0000000000000..aa41edcf40d41 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/user_entities/user_entities.json @@ -0,0 +1,51 @@ +{ + "id": "user_ent", + "description": "[user.name entities] grouped by @timestamp and aggregated on user.name, and event.categories of success, failure, and unknown", + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + }, + "user.name": { + "terms": { + "field": "user.name" + } + } + }, + "aggregations": { + "metrics.event.authentication": { + "filter": { + "term": { + "event.category": "authentication" + } + }, + "aggs": { + "success.value_count": { + "filter": { + "term": { + "event.outcome": "success" + } + } + }, + "failure.value_count": { + "filter": { + "term": { + "event.outcome": "failure" + } + } + }, + "unknown.value_count": { + "filter": { + "term": { + "event.outcome": "unknown" + } + } + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/user_entities/user_entities_mapping.json b/x-pack/plugins/metrics_entities/server/modules/user_entities/user_entities_mapping.json new file mode 100644 index 0000000000000..2532afa3040c6 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/user_entities/user_entities_mapping.json @@ -0,0 +1,53 @@ +{ + "mappings": { + "_meta": { + "index": "user_ent" + }, + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "metrics": { + "properties": { + "event": { + "properties": { + "authentication": { + "properties": { + "failure": { + "properties": { + "value_count": { + "type": "long" + } + } + }, + "success": { + "properties": { + "value_count": { + "type": "long" + } + } + }, + "unknown": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "user": { + "properties": { + "name": { + "type": "keyword" + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/user_metrics/index.ts b/x-pack/plugins/metrics_entities/server/modules/user_metrics/index.ts new file mode 100644 index 0000000000000..b7c6e65155ed2 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/user_metrics/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import userMetrics from './user_metrics.json'; +import userMetricsMapping from './user_metrics_mapping.json'; + +export { userMetrics, userMetricsMapping }; diff --git a/x-pack/plugins/metrics_entities/server/modules/user_metrics/user_metrics.json b/x-pack/plugins/metrics_entities/server/modules/user_metrics/user_metrics.json new file mode 100644 index 0000000000000..86154bd8c68ec --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/user_metrics/user_metrics.json @@ -0,0 +1,56 @@ +{ + "id": "user_met", + "description": "[event.category authentication metrics] grouped by @timestamp and aggregated on success, failure, and unknown", + "source": { + "query": { + "bool": { + "filter": [ + { + "bool": { + "filter": [ + { + "term": { + "event.category": "authentication" + } + } + ] + } + } + ] + } + } + }, + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1h" + } + } + }, + "aggregations": { + "metrics.event.authentication.success.value_count": { + "filter": { + "term": { + "event.outcome": "success" + } + } + }, + "metrics.event.authentication.failure.value_count": { + "filter": { + "term": { + "event.outcome": "failure" + } + } + }, + "metrics.event.authentication.unknown.value_count": { + "filter": { + "term": { + "event.outcome": "unknown" + } + } + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/modules/user_metrics/user_metrics_mapping.json b/x-pack/plugins/metrics_entities/server/modules/user_metrics/user_metrics_mapping.json new file mode 100644 index 0000000000000..c63dcd2b4a429 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/modules/user_metrics/user_metrics_mapping.json @@ -0,0 +1,46 @@ +{ + "mappings": { + "_meta": { + "index": "user_met" + }, + "dynamic": "strict", + "properties": { + "metrics": { + "properties": { + "event": { + "properties": { + "authentication": { + "properties": { + "failure": { + "properties": { + "value_count": { + "type": "long" + } + } + }, + "success": { + "properties": { + "value_count": { + "type": "long" + } + } + }, + "unknown": { + "properties": { + "value_count": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "@timestamp": { + "type": "date" + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/plugin.ts b/x-pack/plugins/metrics_entities/server/plugin.ts new file mode 100644 index 0000000000000..73d4ffc6367fe --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/plugin.ts @@ -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 { + CoreSetup, + CoreStart, + Logger, + Plugin, + PluginInitializerContext, +} from '../../../../src/core/server'; + +import { + ContextProvider, + ContextProviderReturn, + MetricsEntitiesPluginSetup, + MetricsEntitiesPluginStart, + MetricsEntitiesRequestHandlerContext, +} from './types'; +import { getTransforms, postTransforms } from './routes'; +import { MetricsEntitiesClient } from './services/metrics_entities_client'; +import { deleteTransforms } from './routes/delete_transforms'; + +export class MetricsEntitiesPlugin + implements Plugin { + private readonly logger: Logger; + private kibanaVersion: string; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.kibanaVersion = initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup): MetricsEntitiesPluginSetup { + const router = core.http.createRouter(); + + core.http.registerRouteHandlerContext( + 'metricsEntities', + this.createRouteHandlerContext() + ); + + // Register server side APIs + // TODO: Add all of these into a separate file and call that file called init_routes.ts + getTransforms(router); + postTransforms(router); + deleteTransforms(router); + + return { + getMetricsEntitiesClient: (esClient): MetricsEntitiesClient => + new MetricsEntitiesClient({ + esClient, + kibanaVersion: this.kibanaVersion, + logger: this.logger, + }), + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public start(core: CoreStart): void { + this.logger.debug('Starting plugin'); + } + + public stop(): void { + this.logger.debug('Stopping plugin'); + } + + private createRouteHandlerContext = (): ContextProvider => { + return async (context): ContextProviderReturn => { + const { + core: { + elasticsearch: { + client: { asCurrentUser: esClient }, + }, + }, + } = context; + return { + getMetricsEntitiesClient: (): MetricsEntitiesClient => + new MetricsEntitiesClient({ + esClient, + kibanaVersion: this.kibanaVersion, + logger: this.logger, + }), + }; + }; + }; +} diff --git a/x-pack/plugins/metrics_entities/server/routes/delete_transforms.ts b/x-pack/plugins/metrics_entities/server/routes/delete_transforms.ts new file mode 100644 index 0000000000000..f5236e462dd81 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/routes/delete_transforms.ts @@ -0,0 +1,50 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { IRouter } from '../../../../../src/core/server'; +import { METRICS_ENTITIES_TRANSFORMS } from '../../common/constants'; +import { ModuleNames } from '../modules'; + +import { getMetricsEntitiesClient } from './utils/get_metrics_entities_client'; + +/** + * Deletes transforms. + * NOTE: We use a POST rather than a DELETE on purpose here to ensure that there + * are not problems with the body being sent. + * @param router The router to delete the collection of transforms + */ +export const deleteTransforms = (router: IRouter): void => { + router.post( + { + path: `${METRICS_ENTITIES_TRANSFORMS}/_delete`, + validate: { + // TODO: Add the validation instead of allowing handler to have access to raw non-validated in runtime + body: schema.object({}, { unknowns: 'allow' }), + query: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, request, response) => { + // TODO: Type this through validation above and remove the weird casting of: "as { modules: ModuleNames };" + // TODO: Validate for runtime that the module exists or not and throw before pushing the module name lower + // TODO: Change modules to be part of the body and become an array of values + // TODO: Wrap this in a try catch block and report errors + const { modules, prefix = '', suffix = '' } = request.body as { + modules: ModuleNames[]; + prefix: string; + suffix: string; + }; + const metrics = getMetricsEntitiesClient(context); + await metrics.deleteTransforms({ modules, prefix, suffix }); + + return response.custom({ + statusCode: 204, + }); + } + ); +}; diff --git a/x-pack/plugins/metrics_entities/server/routes/get_transforms.ts b/x-pack/plugins/metrics_entities/server/routes/get_transforms.ts new file mode 100644 index 0000000000000..cda61512ce293 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/routes/get_transforms.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '../../../../../src/core/server'; +import { METRICS_ENTITIES_TRANSFORMS } from '../../common/constants'; + +import { getMetricsEntitiesClient } from './utils/get_metrics_entities_client'; + +/** + * Returns all transforms from all modules + * TODO: Add support for specific modules and prefix + * @param router The router to get the collection of transforms + */ +export const getTransforms = (router: IRouter): void => { + router.get( + { + path: METRICS_ENTITIES_TRANSFORMS, + // TODO: Add the validation instead of false + // TODO: Add the prefix and module support + validate: false, + }, + async (context, _, response) => { + const metrics = getMetricsEntitiesClient(context); + const summaries = await metrics.getTransforms(); + return response.ok({ + body: { + summaries, + }, + }); + } + ); +}; diff --git a/x-pack/plugins/metrics_entities/server/routes/index.ts b/x-pack/plugins/metrics_entities/server/routes/index.ts new file mode 100644 index 0000000000000..9470772f46d70 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/routes/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './delete_transforms'; +export * from './get_transforms'; +export * from './post_transforms'; diff --git a/x-pack/plugins/metrics_entities/server/routes/post_transforms.ts b/x-pack/plugins/metrics_entities/server/routes/post_transforms.ts new file mode 100644 index 0000000000000..d5b5648757e8b --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/routes/post_transforms.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { IRouter } from '../../../../../src/core/server'; +import { METRICS_ENTITIES_TRANSFORMS } from '../../common/constants'; +import { ModuleNames } from '../modules'; + +import { getMetricsEntitiesClient } from './utils/get_metrics_entities_client'; + +/** + * Creates transforms. + * @param router The router to get the collection of transforms + */ +export const postTransforms = (router: IRouter): void => { + router.post( + { + path: METRICS_ENTITIES_TRANSFORMS, + validate: { + // TODO: Add the validation instead of allowing handler to have access to raw non-validated in runtime + body: schema.object({}, { unknowns: 'allow' }), + query: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, request, response) => { + // TODO: Type this through validation above and remove the weird casting of: "as { modules: ModuleNames };" + // TODO: Validate for runtime that the module exists or not and throw before pushing the module name lower + // TODO: Change modules to be part of the body and become an array of values + // TODO: Wrap this in a try catch block and report errors + const { + modules, + auto_start: autoStart = false, + settings: { + max_page_search_size: maxPageSearchSize = 500, + docs_per_second: docsPerSecond = null, + } = { + docsPerSecond: null, + maxPageSearchSize: 500, + }, + frequency = '1m', + indices, + query = { match_all: {} }, + prefix = '', + suffix = '', + sync = { + time: { + delay: '60s', + field: '@timestamp', + }, + }, + } = request.body as { + modules: ModuleNames[]; + auto_start: boolean; + indices: string[]; + // We can blow up at 65 character+ for transform id. We need to validate the prefix + transform jobs and return an error + prefix: string; + query: object; + suffix: string; + frequency: string; + settings: { + max_page_search_size: number; + docs_per_second: number; + }; + sync: { + time: { + delay: string; + field: string; + }; + }; + }; + const metrics = getMetricsEntitiesClient(context); + await metrics.postTransforms({ + autoStart, + docsPerSecond, + frequency, + indices, + maxPageSearchSize, + modules, + prefix, + query, + suffix, + sync, + }); + + return response.custom({ + body: { acknowledged: true }, + statusCode: 201, + }); + } + ); +}; diff --git a/x-pack/plugins/metrics_entities/server/routes/utils/get_metrics_entities_client.ts b/x-pack/plugins/metrics_entities/server/routes/utils/get_metrics_entities_client.ts new file mode 100644 index 0000000000000..fdbbd98128741 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/routes/utils/get_metrics_entities_client.ts @@ -0,0 +1,21 @@ +/* + * 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 { ErrorWithStatusCode } from '../../error_with_status_code'; +import { MetricsEntitiesClient } from '../../services/metrics_entities_client'; +import type { MetricsEntitiesRequestHandlerContext } from '../../types'; + +export const getMetricsEntitiesClient = ( + context: MetricsEntitiesRequestHandlerContext +): MetricsEntitiesClient => { + const metricsEntities = context.metricsEntities?.getMetricsEntitiesClient(); + if (metricsEntities == null) { + throw new ErrorWithStatusCode('Metrics Entities is not found as a plugin', 404); + } else { + return metricsEntities; + } +}; diff --git a/x-pack/plugins/metrics_entities/server/routes/utils/index.ts b/x-pack/plugins/metrics_entities/server/routes/utils/index.ts new file mode 100644 index 0000000000000..eee678d64b30d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/routes/utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './get_metrics_entities_client'; diff --git a/x-pack/plugins/metrics_entities/server/scripts/check_env_variables.sh b/x-pack/plugins/metrics_entities/server/scripts/check_env_variables.sh new file mode 100755 index 0000000000000..df2354ed8398a --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/check_env_variables.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# +# 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. +# + +# Add this to the start of any scripts to detect if env variables are set + +set -e + +if [ -z "${ELASTICSEARCH_USERNAME}" ]; then + echo "Set ELASTICSEARCH_USERNAME in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_PASSWORD}" ]; then + echo "Set ELASTICSEARCH_PASSWORD in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_URL}" ]; then + echo "Set ELASTICSEARCH_URL in your environment" + exit 1 +fi + +if [ -z "${KIBANA_URL}" ]; then + echo "Set KIBANA_URL in your environment" + exit 1 +fi diff --git a/x-pack/plugins/metrics_entities/server/scripts/delete_examples/all.json b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/all.json new file mode 100644 index 0000000000000..b07028d0cab89 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/all.json @@ -0,0 +1,11 @@ +{ + "prefix": "all", + "modules": [ + "host_metrics", + "host_entities", + "network_metrics", + "network_entities", + "user_entities", + "user_metrics" + ] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/delete_examples/all_prefix_auditbeat.json b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/all_prefix_auditbeat.json new file mode 100644 index 0000000000000..5b20203075924 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/all_prefix_auditbeat.json @@ -0,0 +1,11 @@ +{ + "prefix": "auditbeat", + "modules": [ + "host_metrics", + "host_entities", + "network_metrics", + "network_entities", + "user_entities", + "user_metrics" + ] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/delete_examples/network_entities_auditbeat.json b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/network_entities_auditbeat.json new file mode 100644 index 0000000000000..b1e21ebbc9bd6 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/network_entities_auditbeat.json @@ -0,0 +1,3 @@ +{ + "modules": ["network_entities"] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/delete_examples/one_module.json b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/one_module.json new file mode 100644 index 0000000000000..2e9a62b9fbe82 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/one_module.json @@ -0,0 +1,3 @@ +{ + "modules": ["user_entities"] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/delete_examples/two_modules.json b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/two_modules.json new file mode 100644 index 0000000000000..e3292834f3d08 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/delete_examples/two_modules.json @@ -0,0 +1,3 @@ +{ + "modules": ["host_metrics", "host_entities"] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/delete_transforms.sh b/x-pack/plugins/metrics_entities/server/scripts/delete_transforms.sh new file mode 100755 index 0000000000000..d4c03411cbcca --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/delete_transforms.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +FILE=${1:-./post_examples/one_module.json} + +# Example: ./delete_transforms.sh ./delete_examples/one_module.json +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/metrics_entities/transforms/_delete \ + -d @${FILE} \ + | jq . diff --git a/x-pack/plugins/metrics_entities/server/scripts/get_transforms.sh b/x-pack/plugins/metrics_entities/server/scripts/get_transforms.sh new file mode 100755 index 0000000000000..34f7e4b83cc39 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/get_transforms.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_transforms.sh +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/metrics_entities/transforms | jq . diff --git a/x-pack/plugins/metrics_entities/server/scripts/hard_reset.sh b/x-pack/plugins/metrics_entities/server/scripts/hard_reset.sh new file mode 100755 index 0000000000000..69acf10764936 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/hard_reset.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# +# 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. +# + +# TODO Make this work + +set -e +./check_env_variables.sh + +# remove all templates +# add all templates again and start them + diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/all.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/all.json new file mode 100644 index 0000000000000..dac53a63dad55 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/all.json @@ -0,0 +1,32 @@ +{ + "prefix": "all", + "modules": [ + "host_metrics", + "host_entities", + "network_metrics", + "network_entities", + "user_entities", + "user_metrics" + ], + "indices": [ + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + "-*elastic-cloud-logs-*" + ], + "auto_start": true, + "settings": { + "max_page_search_size": 5000 + }, + "query": { + "range": { + "@timestamp": { + "gte": "now-1d/d", + "format": "strict_date_optional_time" + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/all_auditbeat.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/all_auditbeat.json new file mode 100644 index 0000000000000..5a2f6b5024689 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/all_auditbeat.json @@ -0,0 +1,23 @@ +{ + "modules": [ + "host_metrics", + "host_entities", + "network_metrics", + "network_entities", + "user_entities", + "user_metrics" + ], + "indices": ["auditbeat-*"], + "auto_start": true, + "settings": { + "max_page_search_size": 5000 + }, + "query": { + "range": { + "@timestamp": { + "gte": "now-1d/d", + "format": "strict_date_optional_time" + } + } + } +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/network_entities_auditbeat.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/network_entities_auditbeat.json new file mode 100644 index 0000000000000..379a5733a91f9 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/network_entities_auditbeat.json @@ -0,0 +1,4 @@ +{ + "modules": ["network_entities"], + "indices": ["auditbeat-*"] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_allindices_autostart.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_allindices_autostart.json new file mode 100644 index 0000000000000..9872706ff1ac2 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_allindices_autostart.json @@ -0,0 +1,24 @@ +{ + "modules": ["network_metrics"], + "indices": [ + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + "-*elastic-cloud-logs-*" + ], + "auto_start": true, + "query": { + "range": { + "@timestamp": { + "gte": "now-1d/d", + "format": "strict_date_optional_time" + } + } + }, + "settings": { + "max_page_search_size": 5000 + } +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_auditbeat.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_auditbeat.json new file mode 100644 index 0000000000000..4ce4db5da9f23 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_auditbeat.json @@ -0,0 +1,16 @@ +{ + "modules": ["network_metrics"], + "indices": ["auditbeat-*"], + "auto_start": true, + "query": { + "range": { + "@timestamp": { + "gte": "now-1d/d", + "format": "strict_date_optional_time" + } + } + }, + "settings": { + "max_page_search_size": 5000 + } +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_auto_start.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_auto_start.json new file mode 100644 index 0000000000000..d5a87c80a44a0 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_auto_start.json @@ -0,0 +1,8 @@ +{ + "modules": ["host_metrics"], + "indices": ["auditbeat-*"], + "auto_start": true, + "settings": { + "max_page_search_size": 5000 + } +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_prefix_auditbeat.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_prefix_auditbeat.json new file mode 100644 index 0000000000000..f20875f28ffa3 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/one_module_prefix_auditbeat.json @@ -0,0 +1,5 @@ +{ + "modules": ["host_metrics"], + "indices": ["auditbeat-*"], + "prefix": ["default_"] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/two_modules_all.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/two_modules_all.json new file mode 100644 index 0000000000000..8ec9401b94433 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/two_modules_all.json @@ -0,0 +1,24 @@ +{ + "modules": ["network_metrics", "network_entities"], + "indices": [ + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + "-*elastic-cloud-logs-*" + ], + "auto_start": true, + "query": { + "range": { + "@timestamp": { + "gte": "now-1d/d", + "format": "strict_date_optional_time" + } + } + }, + "settings": { + "max_page_search_size": 5000 + } +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_examples/two_modules_auditbeat.json b/x-pack/plugins/metrics_entities/server/scripts/post_examples/two_modules_auditbeat.json new file mode 100644 index 0000000000000..5229cd88fc433 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_examples/two_modules_auditbeat.json @@ -0,0 +1,4 @@ +{ + "modules": ["host_metrics", "host_entities"], + "indices": ["auditbeat-*"] +} diff --git a/x-pack/plugins/metrics_entities/server/scripts/post_transforms.sh b/x-pack/plugins/metrics_entities/server/scripts/post_transforms.sh new file mode 100755 index 0000000000000..9dd4169cc01d6 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/post_transforms.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +FILE=${1:-./post_examples/one_module_auditbeat.json} + +# Example: ./post_transforms.sh ./post_examples/one_module_auditbeat.json +# Example: ./post_transforms.sh ./post_examples/one_module_namespace_auditbeat.json +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/metrics_entities/transforms \ + -d @${FILE} \ + | jq . diff --git a/x-pack/plugins/metrics_entities/server/scripts/update_transforms.sh b/x-pack/plugins/metrics_entities/server/scripts/update_transforms.sh new file mode 100755 index 0000000000000..bccf49e2d1b0d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/scripts/update_transforms.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# TODO Make this work diff --git a/x-pack/plugins/metrics_entities/server/services/delete_transforms.ts b/x-pack/plugins/metrics_entities/server/services/delete_transforms.ts new file mode 100644 index 0000000000000..ef172bcbf7c02 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/delete_transforms.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; + +import { ModuleNames, installableMappings, installableTransforms } from '../modules'; +import type { Logger } from '../../../../../src/core/server'; + +import { uninstallMappings } from './uninstall_mappings'; +import { uninstallTransforms } from './uninstall_transforms'; + +interface DeleteTransformsOptions { + esClient: ElasticsearchClient; + logger: Logger; + modules: ModuleNames[]; + prefix: string; + suffix: string; +} + +export const deleteTransforms = async ({ + esClient, + logger, + modules, + prefix, + suffix, +}: DeleteTransformsOptions): Promise => { + for (const moduleName of modules) { + const mappings = installableMappings[moduleName]; + const transforms = installableTransforms[moduleName]; + + await uninstallTransforms({ esClient, logger, prefix, suffix, transforms }); + await uninstallMappings({ esClient, logger, mappings, prefix, suffix }); + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/get_transforms.ts b/x-pack/plugins/metrics_entities/server/services/get_transforms.ts new file mode 100644 index 0000000000000..08189f4b3361a --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/get_transforms.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; + +import type { Logger } from '../../../../../src/core/server'; + +interface GetTransformsOptions { + esClient: ElasticsearchClient; + logger: Logger; +} + +// TODO: Type the Promise to a stronger type +export const getTransforms = async ({ esClient }: GetTransformsOptions): Promise => { + const { body } = await esClient.transform.getTransform({ + size: 1000, + transform_id: '*', + }); + return body; +}; diff --git a/x-pack/plugins/metrics_entities/server/services/index.ts b/x-pack/plugins/metrics_entities/server/services/index.ts new file mode 100644 index 0000000000000..71611d2a5eae0 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './delete_transforms'; +export * from './get_transforms'; +export * from './install_mappings'; +export * from './install_transforms'; +export * from './metrics_entities_client'; +export * from './post_transforms'; +export * from './uninstall_mappings'; +export * from './uninstall_transforms'; diff --git a/x-pack/plugins/metrics_entities/server/services/install_mappings.ts b/x-pack/plugins/metrics_entities/server/services/install_mappings.ts new file mode 100644 index 0000000000000..da42f9916ff9b --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/install_mappings.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; + +import { Mappings } from '../modules/types'; +import type { Logger } from '../../../../../src/core/server'; + +import { + computeMappingId, + getIndexExists, + logMappingDebug, + logMappingError, + logMappingInfo, +} from './utils'; + +interface CreateMappingOptions { + esClient: ElasticsearchClient; + mappings: Mappings[]; + prefix: string; + suffix: string; + logger: Logger; + kibanaVersion: string; +} + +export const installMappings = async ({ + esClient, + kibanaVersion, + mappings, + prefix, + suffix, + logger, +}: CreateMappingOptions): Promise => { + for (const mapping of mappings) { + const { index } = mapping.mappings._meta; + const mappingId = computeMappingId({ id: index, prefix, suffix }); + const exists = await getIndexExists(esClient, mappingId); + const computedBody = { + ...mapping, + ...{ + mappings: { + ...mapping.mappings, + _meta: { + ...mapping.mappings._meta, + ...{ + created_by: 'metrics_entities', + index: mappingId, + version: kibanaVersion, + }, + }, + }, + }, + }; + if (!exists) { + try { + logMappingInfo({ id: mappingId, logger, message: 'does not exist, creating the mapping' }); + await esClient.indices.create({ + body: computedBody, + index: mappingId, + }); + } catch (error) { + logMappingError({ + error, + id: mappingId, + logger, + message: 'cannot install mapping', + postBody: computedBody, + }); + } + } else { + logMappingDebug({ + id: mappingId, + logger, + message: 'mapping already exists. It will not be recreated', + }); + } + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/install_transforms.ts b/x-pack/plugins/metrics_entities/server/services/install_transforms.ts new file mode 100644 index 0000000000000..d0a81955ca184 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/install_transforms.ts @@ -0,0 +1,122 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; + +import { Transforms } from '../modules/types'; +import type { Logger } from '../../../../../src/core/server'; + +import { + computeMappingId, + computeTransformId, + getTransformExists, + logTransformDebug, + logTransformError, + logTransformInfo, +} from './utils'; + +interface CreateTransformOptions { + esClient: ElasticsearchClient; + transforms: Transforms[]; + autoStart: boolean; + indices: string[]; + frequency: string; + logger: Logger; + query: object; + docsPerSecond: number | null; + maxPageSearchSize: number; + sync: { + time: { + delay: string; + field: string; + }; + }; + prefix: string; + suffix: string; +} + +export const installTransforms = async ({ + autoStart, + esClient, + frequency, + indices, + docsPerSecond, + logger, + maxPageSearchSize, + prefix, + suffix, + transforms, + query, + sync, +}: CreateTransformOptions): Promise => { + for (const transform of transforms) { + const destIndex = transform?.dest?.index ?? transform.id; + const computedMappingIndex = computeMappingId({ id: destIndex, prefix, suffix }); + const { id, ...transformNoId } = { + ...transform, + ...{ source: { ...transform.source, index: indices, query } }, + ...{ dest: { ...transform.dest, index: computedMappingIndex } }, + ...{ + settings: { + ...transform.settings, + docs_per_second: docsPerSecond, + max_page_search_size: maxPageSearchSize, + }, + }, + frequency, + sync, + }; + + const computedName = computeTransformId({ id, prefix, suffix }); + const exists = await getTransformExists(esClient, computedName); + if (!exists) { + try { + logTransformInfo({ + id: computedName, + logger, + message: 'does not exist, creating the transform', + }); + await esClient.transform.putTransform({ + body: transformNoId, + defer_validation: true, + transform_id: computedName, + }); + + if (autoStart) { + logTransformInfo({ + id: computedName, + logger, + message: 'is being auto started', + }); + await esClient.transform.startTransform({ + transform_id: computedName, + }); + } else { + logTransformInfo({ + id: computedName, + logger, + message: 'is not being auto started', + }); + } + } catch (error) { + logTransformError({ + error, + id: computedName, + logger, + message: 'Could not create and/or start', + postBody: transformNoId, + }); + } + } else { + logTransformDebug({ + id: computedName, + logger, + message: 'already exists. It will not be recreated', + }); + } + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/metrics_entities_client.ts b/x-pack/plugins/metrics_entities/server/services/metrics_entities_client.ts new file mode 100644 index 0000000000000..3905503df876d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/metrics_entities_client.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; + +import type { Logger } from '../../../../../src/core/server'; + +import { getTransforms } from './get_transforms'; +import { + ConstructorOptions, + DeleteTransformsOptions, + PostTransformsOptions, +} from './metrics_entities_client_types'; +import { postTransforms } from './post_transforms'; +import { deleteTransforms } from './delete_transforms'; + +export class MetricsEntitiesClient { + private readonly esClient: ElasticsearchClient; + private readonly logger: Logger; + private readonly kibanaVersion: string; + + constructor({ esClient, logger, kibanaVersion }: ConstructorOptions) { + this.esClient = esClient; + this.logger = logger; + this.kibanaVersion = kibanaVersion; + } + + // TODO: Type the unknown to be stronger + public getTransforms = async (): Promise => { + const { esClient, logger } = this; + return getTransforms({ esClient, logger }); + }; + + public postTransforms = async ({ + autoStart, + frequency, + docsPerSecond, + maxPageSearchSize, + modules, + indices, + prefix, + suffix, + query, + sync, + }: PostTransformsOptions): Promise => { + const { esClient, logger, kibanaVersion } = this; + return postTransforms({ + autoStart, + docsPerSecond, + esClient, + frequency, + indices, + kibanaVersion, + logger, + maxPageSearchSize, + modules, + prefix, + query, + suffix, + sync, + }); + }; + + public deleteTransforms = async ({ + modules, + prefix, + suffix, + }: DeleteTransformsOptions): Promise => { + const { esClient, logger } = this; + return deleteTransforms({ esClient, logger, modules, prefix, suffix }); + }; +} diff --git a/x-pack/plugins/metrics_entities/server/services/metrics_entities_client_types.ts b/x-pack/plugins/metrics_entities/server/services/metrics_entities_client_types.ts new file mode 100644 index 0000000000000..1ae9f0d7a2f53 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/metrics_entities_client_types.ts @@ -0,0 +1,41 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; + +import type { Logger } from '../../../../../src/core/server'; +import { ModuleNames } from '../modules'; + +export interface ConstructorOptions { + esClient: ElasticsearchClient; + logger: Logger; + kibanaVersion: string; +} + +export interface PostTransformsOptions { + modules: ModuleNames[]; + autoStart: boolean; + frequency: string; + indices: string[]; + docsPerSecond: number | null; + maxPageSearchSize: number; + prefix: string; + query: object; + suffix: string; + sync: { + time: { + delay: string; + field: string; + }; + }; +} + +export interface DeleteTransformsOptions { + modules: ModuleNames[]; + prefix: string; + suffix: string; +} diff --git a/x-pack/plugins/metrics_entities/server/services/post_transforms.ts b/x-pack/plugins/metrics_entities/server/services/post_transforms.ts new file mode 100644 index 0000000000000..1850047ae1e9d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/post_transforms.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; + +import { ModuleNames, installableMappings, installableTransforms } from '../modules'; +import type { Logger } from '../../../../../src/core/server'; + +import { installMappings } from './install_mappings'; +import { installTransforms } from './install_transforms'; + +interface PostTransformsOptions { + logger: Logger; + esClient: ElasticsearchClient; + modules: ModuleNames[]; + autoStart: boolean; + frequency: string; + indices: string[]; + docsPerSecond: number | null; + kibanaVersion: string; + maxPageSearchSize: number; + query: object; + prefix: string; + suffix: string; + sync: { + time: { + delay: string; + field: string; + }; + }; +} + +export const postTransforms = async ({ + autoStart, + logger, + esClient, + frequency, + indices, + docsPerSecond, + kibanaVersion, + maxPageSearchSize, + modules, + prefix, + suffix, + query, + sync, +}: PostTransformsOptions): Promise => { + for (const moduleName of modules) { + const mappings = installableMappings[moduleName]; + const transforms = installableTransforms[moduleName]; + + await installMappings({ esClient, kibanaVersion, logger, mappings, prefix, suffix }); + await installTransforms({ + autoStart, + docsPerSecond, + esClient, + frequency, + indices, + logger, + maxPageSearchSize, + prefix, + query, + suffix, + sync, + transforms, + }); + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/stop_transforms.ts b/x-pack/plugins/metrics_entities/server/services/stop_transforms.ts new file mode 100644 index 0000000000000..18476d8345cf2 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/stop_transforms.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO: Write this diff --git a/x-pack/plugins/metrics_entities/server/services/uninstall_mappings.ts b/x-pack/plugins/metrics_entities/server/services/uninstall_mappings.ts new file mode 100644 index 0000000000000..b2ea9d96cda13 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/uninstall_mappings.ts @@ -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 { ElasticsearchClient } from 'kibana/server'; + +import { Mappings } from '../modules/types'; +import type { Logger } from '../../../../../src/core/server'; + +import { computeMappingId, logMappingInfo } from './utils'; +import { logMappingError } from './utils/log_mapping_error'; + +interface UninstallMappingOptions { + esClient: ElasticsearchClient; + mappings: Mappings[]; + prefix: string; + suffix: string; + logger: Logger; +} + +export const uninstallMappings = async ({ + esClient, + logger, + mappings, + prefix, + suffix, +}: UninstallMappingOptions): Promise => { + const indices = mappings.map((mapping) => { + const { index } = mapping.mappings._meta; + return computeMappingId({ id: index, prefix, suffix }); + }); + logMappingInfo({ + id: indices.join(), + logger, + message: 'deleting indices', + }); + try { + await esClient.indices.delete({ + allow_no_indices: true, + ignore_unavailable: true, + index: indices, + }); + } catch (error) { + logMappingError({ + error, + id: indices.join(), + logger, + message: 'could not delete index', + postBody: undefined, + }); + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/uninstall_transforms.ts b/x-pack/plugins/metrics_entities/server/services/uninstall_transforms.ts new file mode 100644 index 0000000000000..11f12541bda0d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/uninstall_transforms.ts @@ -0,0 +1,92 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; + +import { Transforms } from '../modules/types'; +import type { Logger } from '../../../../../src/core/server'; + +import { + computeTransformId, + getTransformExists, + logTransformError, + logTransformInfo, +} from './utils'; + +interface UninstallTransformsOptions { + esClient: ElasticsearchClient; + transforms: Transforms[]; + prefix: string; + suffix: string; + logger: Logger; +} + +/** + * Uninstalls all the transforms underneath a given module + */ +export const uninstallTransforms = async ({ + esClient, + logger, + prefix, + suffix, + transforms, +}: UninstallTransformsOptions): Promise => { + transforms.forEach(async (transform) => { + const { id } = transform; + const computedId = computeTransformId({ id, prefix, suffix }); + const exists = await getTransformExists(esClient, computedId); + if (exists) { + logTransformInfo({ + id: computedId, + logger, + message: 'stopping transform', + }); + try { + await esClient.transform.stopTransform({ + allow_no_match: true, + force: true, + timeout: '5s', + transform_id: computedId, + wait_for_completion: true, + }); + } catch (error) { + logTransformError({ + error, + id: computedId, + logger, + message: 'Could not stop transform, still attempting to delete it', + postBody: undefined, + }); + } + logTransformInfo({ + id: computedId, + logger, + message: 'deleting transform', + }); + try { + await esClient.transform.deleteTransform({ + force: true, + transform_id: computedId, + }); + } catch (error) { + logTransformError({ + error, + id: computedId, + logger, + message: 'Could not create and/or start', + postBody: undefined, + }); + } + } else { + logTransformInfo({ + id: computedId, + logger, + message: 'transform does not exist to delete', + }); + } + }); +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/compute_mapping_index.ts b/x-pack/plugins/metrics_entities/server/services/utils/compute_mapping_index.ts new file mode 100644 index 0000000000000..bb1a7720fc575 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/compute_mapping_index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { computeTransformId } from './compute_transform_id'; + +export const computeMappingId = ({ + prefix, + id, + suffix, +}: { + prefix: string; + id: string; + suffix: string; +}): string => { + // TODO: This causes issues if above 65 character limit. We should limit the prefix + // and anything else on the incoming routes to avoid this causing an issue. We should still + // throw here in case I change the prefix or other names and cause issues. + const computedId = computeTransformId({ id, prefix, suffix }); + return `.${computedId}`; +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/compute_transform_id.ts b/x-pack/plugins/metrics_entities/server/services/utils/compute_transform_id.ts new file mode 100644 index 0000000000000..20951b0e447ff --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/compute_transform_id.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_NAME } from '../../../common'; + +export const computeTransformId = ({ + prefix, + id, + suffix, +}: { + prefix: string; + id: string; + suffix: string; +}): string => { + const prefixExists = prefix.trim() !== ''; + const suffixExists = suffix.trim() !== ''; + + // TODO: Check for invalid characters on the main route for prefixExists and suffixExists and do an invalidation + // if either have invalid characters for a job name. Might want to add that same check within the API too at a top level? + if (prefixExists && suffixExists) { + return `${ELASTIC_NAME}_${prefix}_${id}_${suffix}`; + } else if (prefixExists) { + return `${ELASTIC_NAME}_${prefix}_${id}`; + } else if (suffixExists) { + return `${ELASTIC_NAME}_${id}_${suffix}`; + } else { + return `${ELASTIC_NAME}_${id}`; + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/get_index_exists.ts b/x-pack/plugins/metrics_entities/server/services/utils/get_index_exists.ts new file mode 100644 index 0000000000000..bcc37ce047d24 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/get_index_exists.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; + +/** + * Tried and true, copied forever again and again, the way we check if an index exists + * with the least amount of privileges. + * @param esClient The client to check if the index already exists + * @param index The index to check for + * @returns true if it exists, otherwise false + */ +export const getIndexExists = async ( + esClient: ElasticsearchClient, + index: string +): Promise => { + try { + const { body: response } = await esClient.search({ + allow_no_indices: true, + body: { + terminate_after: 1, + }, + index, + size: 0, + }); + return response._shards.total > 0; + } catch (err) { + if (err.body?.status === 404) { + return false; + } else { + throw err.body ? err.body : err; + } + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/get_json.ts b/x-pack/plugins/metrics_entities/server/services/utils/get_json.ts new file mode 100644 index 0000000000000..71853f2a4ee66 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/get_json.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO: Move indent to configuration part or flip to default false +export const getJSON = (body: unknown, indent: boolean = true): string => + indent ? JSON.stringify(body, null, 2) : JSON.stringify(body); diff --git a/x-pack/plugins/metrics_entities/server/services/utils/get_transform_exists.ts b/x-pack/plugins/metrics_entities/server/services/utils/get_transform_exists.ts new file mode 100644 index 0000000000000..4dffce5f4ecbe --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/get_transform_exists.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; + +export const getTransformExists = async ( + esClient: ElasticsearchClient, + id: string +): Promise => { + try { + const { + body: { count }, + } = await esClient.transform.getTransform({ + size: 1000, + transform_id: id, + }); + return count > 0; + } catch (err) { + if (err.body?.status === 404) { + return false; + } else { + throw err.body ? err.body : err; + } + } +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/index.ts b/x-pack/plugins/metrics_entities/server/services/utils/index.ts new file mode 100644 index 0000000000000..0871c1bf3f7b4 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './compute_mapping_index'; +export * from './compute_transform_id'; +export * from './get_index_exists'; +export * from './get_transform_exists'; +export * from './log_mapping_debug'; +export * from './log_mapping_error'; +export * from './log_mapping_info'; +export * from './log_transform_debug'; +export * from './log_transform_error'; +export * from './log_transform_info'; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_debug.ts b/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_debug.ts new file mode 100644 index 0000000000000..f3c56aac900f1 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_debug.ts @@ -0,0 +1,20 @@ +/* + * 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 { Logger } from '../../../../../../src/core/server'; + +export const logMappingDebug = ({ + logger, + id, + message, +}: { + logger: Logger; + id: string; + message: string; +}): void => { + logger.debug(`mapping id: "${id}", ${message}`); +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_error.ts b/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_error.ts new file mode 100644 index 0000000000000..43ae07619318c --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_error.ts @@ -0,0 +1,27 @@ +/* + * 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 { Logger } from '../../../../../../src/core/server'; + +import { getJSON } from './get_json'; + +export const logMappingError = ({ + logger, + id, + message, + error, + postBody, +}: { + logger: Logger; + id: string; + error: unknown; + message: string; + postBody: {} | undefined; +}): void => { + const postString = postBody != null ? `, post body: "${getJSON(postBody)}"` : ''; + logger.error(`${message}, mapping id: "${id}"${postString}, error: ${getJSON(error)}`); +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_info.ts b/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_info.ts new file mode 100644 index 0000000000000..e75c380aad38a --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/log_mapping_info.ts @@ -0,0 +1,20 @@ +/* + * 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 { Logger } from '../../../../../../src/core/server'; + +export const logMappingInfo = ({ + logger, + id, + message, +}: { + logger: Logger; + id: string; + message: string; +}): void => { + logger.info(`mapping id: "${id}", ${message}`); +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/log_transform_debug.ts b/x-pack/plugins/metrics_entities/server/services/utils/log_transform_debug.ts new file mode 100644 index 0000000000000..61c5dd0b37947 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/log_transform_debug.ts @@ -0,0 +1,20 @@ +/* + * 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 { Logger } from '../../../../../../src/core/server'; + +export const logTransformDebug = ({ + logger, + id, + message, +}: { + logger: Logger; + id: string; + message: string; +}): void => { + logger.debug(`transform id: "${id}", ${message}`); +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/log_transform_error.ts b/x-pack/plugins/metrics_entities/server/services/utils/log_transform_error.ts new file mode 100644 index 0000000000000..2d883ca68be75 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/log_transform_error.ts @@ -0,0 +1,27 @@ +/* + * 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 { Logger } from '../../../../../../src/core/server'; + +import { getJSON } from './get_json'; + +export const logTransformError = ({ + id, + logger, + error, + postBody, + message, +}: { + logger: Logger; + id: string; + error: unknown; + message: string; + postBody: {} | undefined; +}): void => { + const postString = postBody != null ? `, post body: "${getJSON(postBody)}"` : ''; + logger.error(`${message}, transform id: ${id}${postString}, response error: ${getJSON(error)}`); +}; diff --git a/x-pack/plugins/metrics_entities/server/services/utils/log_transform_info.ts b/x-pack/plugins/metrics_entities/server/services/utils/log_transform_info.ts new file mode 100644 index 0000000000000..1bfb918664007 --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/services/utils/log_transform_info.ts @@ -0,0 +1,20 @@ +/* + * 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 { Logger } from '../../../../../../src/core/server'; + +export const logTransformInfo = ({ + logger, + id, + message, +}: { + logger: Logger; + id: string; + message: string; +}): void => { + logger.info(`transform id: "${id}", ${message}`); +}; diff --git a/x-pack/plugins/metrics_entities/server/types.ts b/x-pack/plugins/metrics_entities/server/types.ts new file mode 100644 index 0000000000000..41df562234c0d --- /dev/null +++ b/x-pack/plugins/metrics_entities/server/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient, IContextProvider, RequestHandlerContext } from 'kibana/server'; + +import { MetricsEntitiesClient } from './services/metrics_entities_client'; + +export type GetMetricsEntitiesClientType = (esClient: ElasticsearchClient) => MetricsEntitiesClient; + +export interface MetricsEntitiesPluginSetup { + getMetricsEntitiesClient: GetMetricsEntitiesClientType; +} + +export type MetricsEntitiesPluginStart = void; + +export type ContextProvider = IContextProvider< + MetricsEntitiesRequestHandlerContext, + 'metricsEntities' +>; + +export interface MetricsEntitiesApiRequestHandlerContext { + getMetricsEntitiesClient: () => MetricsEntitiesClient; +} + +export interface MetricsEntitiesRequestHandlerContext extends RequestHandlerContext { + metricsEntities?: MetricsEntitiesApiRequestHandlerContext; +} + +/** + * @internal + */ +export type ContextProviderReturn = Promise; diff --git a/x-pack/plugins/metrics_entities/tsconfig.json b/x-pack/plugins/metrics_entities/tsconfig.json new file mode 100644 index 0000000000000..15e6aa1601627 --- /dev/null +++ b/x-pack/plugins/metrics_entities/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/ml/public/alerting/config_validator.tsx b/x-pack/plugins/ml/public/alerting/config_validator.tsx index 5a834ab14dd35..cdd696430ca5e 100644 --- a/x-pack/plugins/ml/public/alerting/config_validator.tsx +++ b/x-pack/plugins/ml/public/alerting/config_validator.tsx @@ -13,18 +13,22 @@ import { parseInterval } from '../../common/util/parse_interval'; import { CombinedJobWithStats } from '../../common/types/anomaly_detection_jobs'; import { DATAFEED_STATE } from '../../common/constants/states'; import { MlAnomalyDetectionAlertParams } from '../../common/types/alerts'; +import { MlAnomalyAlertTriggerProps } from './ml_anomaly_alert_trigger'; +import { TOP_N_BUCKETS_COUNT } from '../../common/constants/alerts'; interface ConfigValidatorProps { alertInterval: string; jobConfigs: CombinedJobWithStats[]; alertParams: MlAnomalyDetectionAlertParams; + alertNotifyWhen: MlAnomalyAlertTriggerProps['alertNotifyWhen']; + maxNumberOfBuckets?: number; } /** * Validated alert configuration */ export const ConfigValidator: FC = React.memo( - ({ jobConfigs = [], alertInterval, alertParams }) => { + ({ jobConfigs = [], alertInterval, alertParams, alertNotifyWhen, maxNumberOfBuckets }) => { if (jobConfigs.length === 0) return null; const alertIntervalInSeconds = parseInterval(alertInterval)!.asSeconds(); @@ -41,49 +45,81 @@ export const ConfigValidator: FC = React.memo( const configContainsIssues = isAlertIntervalTooHigh || jobWithoutStartedDatafeed.length > 0; - if (!configContainsIssues) return null; + const notifyWhenWarning = + alertNotifyWhen === 'onActiveAlert' && + lookbackIntervalInSeconds && + alertIntervalInSeconds < lookbackIntervalInSeconds; + + const bucketSpanDuration = parseInterval(jobConfigs[0].analysis_config.bucket_span); + const notificationDuration = bucketSpanDuration + ? Math.ceil(bucketSpanDuration.asMinutes()) * + Math.min( + alertParams.topNBuckets ?? TOP_N_BUCKETS_COUNT, + maxNumberOfBuckets ?? TOP_N_BUCKETS_COUNT + ) + : undefined; return ( <> - - } - color="warning" - size={'s'} - > -
    - {isAlertIntervalTooHigh ? ( -
  • + {configContainsIssues ? ( + <> + -
  • - ) : null} + } + color="warning" + size={'s'} + > +
      + {isAlertIntervalTooHigh ? ( +
    • + +
    • + ) : null} - {jobWithoutStartedDatafeed.length > 0 ? ( -
    • + {jobWithoutStartedDatafeed.length > 0 ? ( +
    • + +
    • + ) : null} +
    + + + + ) : null} + {notifyWhenWarning ? ( + <> + - - ) : null} -
-
- + } + color="warning" + size={'s'} + /> + + + ) : null} ); } diff --git a/x-pack/plugins/ml/public/alerting/interim_results_control.tsx b/x-pack/plugins/ml/public/alerting/interim_results_control.tsx index fa930d9a0ea0f..0b7ad1184f27f 100644 --- a/x-pack/plugins/ml/public/alerting/interim_results_control.tsx +++ b/x-pack/plugins/ml/public/alerting/interim_results_control.tsx @@ -25,7 +25,7 @@ export const InterimResultsControl: FC = React.memo( defaultMessage="Include interim results" /> } - checked={value} + checked={value ?? false} onChange={onChange.bind(null, !value)} /> diff --git a/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx b/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx index 3c8ee6bf4899f..12fbaece54fac 100644 --- a/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx +++ b/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx @@ -29,17 +29,10 @@ import { CombinedJobWithStats } from '../../common/types/anomaly_detection_jobs' import { AdvancedSettings } from './advanced_settings'; import { getLookbackInterval, getTopNBuckets } from '../../common/util/alerts'; import { isDefined } from '../../common/types/guards'; +import { AlertTypeParamsExpressionProps } from '../../../triggers_actions_ui/public'; +import { parseInterval } from '../../common/util/parse_interval'; -interface MlAnomalyAlertTriggerProps { - alertParams: MlAnomalyDetectionAlertParams; - setAlertParams: ( - key: T, - value: MlAnomalyDetectionAlertParams[T] - ) => void; - setAlertProperty: (prop: string, update: Partial) => void; - errors: Record; - alertInterval: string; -} +export type MlAnomalyAlertTriggerProps = AlertTypeParamsExpressionProps; const MlAnomalyAlertTrigger: FC = ({ alertParams, @@ -47,6 +40,7 @@ const MlAnomalyAlertTrigger: FC = ({ setAlertProperty, errors, alertInterval, + alertNotifyWhen, }) => { const { services: { http }, @@ -116,6 +110,8 @@ const MlAnomalyAlertTrigger: FC = ({ includeInterim: false, // Preserve job selection jobSelection, + lookbackInterval: undefined, + topNBuckets: undefined, }); } }); @@ -142,6 +138,20 @@ const MlAnomalyAlertTrigger: FC = ({ }; }, [alertParams, advancedSettings]); + const maxNumberOfBuckets = useMemo(() => { + if (jobConfigs.length === 0) return; + + const bucketDuration = parseInterval(jobConfigs[0].analysis_config.bucket_span); + + const lookbackIntervalDuration = advancedSettings.lookbackInterval + ? parseInterval(advancedSettings.lookbackInterval) + : null; + + if (lookbackIntervalDuration && bucketDuration) { + return Math.ceil(lookbackIntervalDuration.asSeconds() / bucketDuration.asSeconds()); + } + }, [jobConfigs, advancedSettings]); + return ( @@ -164,13 +174,15 @@ const MlAnomalyAlertTrigger: FC = ({ jobsAndGroupIds={jobsAndGroupIds} adJobsApiService={adJobsApiService} onChange={useCallback(onAlertParamChange('jobSelection'), [])} - errors={errors.jobSelection} + errors={Array.isArray(errors.jobSelection) ? errors.jobSelection : []} /> = ({ const sampleSize = ALERT_PREVIEW_SAMPLE_SIZE; const [lookBehindInterval, setLookBehindInterval] = useState(); + const [lastQueryInterval, setLastQueryInterval] = useState(); const [areResultsVisible, setAreResultVisible] = useState(true); const [previewError, setPreviewError] = useState(); const [previewResponse, setPreviewResponse] = useState(); @@ -135,6 +136,7 @@ export const PreviewAlertCondition: FC = ({ sampleSize, }); setPreviewResponse(response); + setLastQueryInterval(lookBehindInterval); setPreviewError(undefined); } catch (e) { setPreviewResponse(undefined); @@ -165,7 +167,7 @@ export const PreviewAlertCondition: FC = ({ label={ } isInvalid={isInvalid} @@ -173,7 +175,7 @@ export const PreviewAlertCondition: FC = ({ > { setLookBehindInterval(e.target.value); }} @@ -220,10 +222,10 @@ export const PreviewAlertCondition: FC = ({ diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts index dc8d019125d2b..0d4d117b69bf3 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts @@ -29,6 +29,7 @@ import { resolveMaxTimeInterval } from '../../../common/util/job_utils'; import { isDefined } from '../../../common/types/guards'; import { getTopNBuckets, resolveLookbackInterval } from '../../../common/util/alerts'; import type { DatafeedsService } from '../../models/job_service/datafeeds'; +import { getEntityFieldName, getEntityFieldValue } from '../../../common/util/anomaly_utils'; type AggResultsResponse = { key?: number } & { [key in PreviewResultsKeys]: { @@ -104,12 +105,20 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da * @param resultType * @param severity */ - const getResultTypeAggRequest = (resultType: AnomalyResultType, severity: number) => { + const getResultTypeAggRequest = ( + resultType: AnomalyResultType, + severity: number, + useInitialScore?: boolean + ) => { + const influencerScoreField = `${useInitialScore ? 'initial_' : ''}influencer_score`; + const recordScoreField = `${useInitialScore ? 'initial_' : ''}record_score`; + const bucketScoreField = `${useInitialScore ? 'initial_' : ''}anomaly_score`; + return { influencer_results: { filter: { range: { - influencer_score: { + [influencerScoreField]: { gte: resultType === ANOMALY_RESULT_TYPE.INFLUENCER ? severity : 0, }, }, @@ -119,7 +128,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da top_hits: { sort: [ { - influencer_score: { + [influencerScoreField]: { order: 'desc', }, }, @@ -141,7 +150,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da score: { script: { lang: 'painless', - source: 'Math.floor(doc["influencer_score"].value)', + source: `Math.floor(doc["${influencerScoreField}"].value)`, }, }, unique_key: { @@ -159,7 +168,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da record_results: { filter: { range: { - record_score: { + [recordScoreField]: { gte: resultType === ANOMALY_RESULT_TYPE.RECORD ? severity : 0, }, }, @@ -169,7 +178,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da top_hits: { sort: [ { - record_score: { + [recordScoreField]: { order: 'desc', }, }, @@ -198,7 +207,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da score: { script: { lang: 'painless', - source: 'Math.floor(doc["record_score"].value)', + source: `Math.floor(doc["${recordScoreField}"].value)`, }, }, unique_key: { @@ -217,7 +226,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da bucket_results: { filter: { range: { - anomaly_score: { + [bucketScoreField]: { gt: severity, }, }, @@ -227,7 +236,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da top_hits: { sort: [ { - anomaly_score: { + [bucketScoreField]: { order: 'desc', }, }, @@ -247,7 +256,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da score: { script: { lang: 'painless', - source: 'Math.floor(doc["anomaly_score"].value)', + source: `Math.floor(doc["${bucketScoreField}"].value)`, }, }, unique_key: { @@ -273,6 +282,18 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da return source.job_id; }; + const getRecordKey = (source: AnomalyRecordDoc): string => { + let alertInstanceKey = `${source.job_id}_${source.timestamp}`; + + const fieldName = getEntityFieldName(source); + const fieldValue = getEntityFieldValue(source); + const entity = + fieldName !== undefined && fieldValue !== undefined ? `_${fieldName}_${fieldValue}` : ''; + alertInstanceKey += `_${source.detector_index}_${source.function}${entity}`; + + return alertInstanceKey; + }; + const getResultsFormatter = (resultType: AnomalyResultType) => { const resultsLabel = getAggResultsLabel(resultType); return (v: AggResultsResponse): AlertExecutionResult | undefined => { @@ -306,7 +327,7 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da return { ...h._source, score: h.fields.score[0], - unique_key: h.fields.unique_key[0], + unique_key: getRecordKey(h._source), }; }) as RecordAnomalyAlertDoc[], topInfluencers: v.influencer_results.top_influencer_hits.hits.hits.map((h) => { @@ -404,11 +425,11 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da alerts_over_time: { date_histogram: { field: 'timestamp', - fixed_interval: lookBackTimeInterval, + fixed_interval: `${maxBucket}s`, // Ignore empty buckets min_doc_count: 1, }, - aggs: getResultTypeAggRequest(params.resultType, params.severity), + aggs: getResultTypeAggRequest(params.resultType, params.severity, true), }, } : getResultTypeAggRequest(params.resultType, params.severity), diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index 0e1e1681373cb..220ae197a15bb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -7,53 +7,91 @@ import { AppDataType, ReportViewTypeId } from '../../types'; import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; +import { + AGENT_HOST_LABEL, + BROWSER_FAMILY_LABEL, + BROWSER_VERSION_LABEL, + CLS_LABEL, + CPU_USAGE_LABEL, + DEVICE_LABEL, + ENVIRONMENT_LABEL, + FCP_LABEL, + FID_LABEL, + HOST_NAME_LABEL, + KIP_OVER_TIME_LABEL, + KPI_LABEL, + LCP_LABEL, + LOCATION_LABEL, + LOGS_FREQUENCY_LABEL, + MEMORY_USAGE_LABEL, + METRIC_LABEL, + MONITOR_DURATION_LABEL, + MONITOR_ID_LABEL, + MONITOR_NAME_LABEL, + MONITOR_STATUS_LABEL, + MONITOR_TYPE_LABEL, + NETWORK_ACTIVITY_LABEL, + OBSERVER_LOCATION_LABEL, + OS_LABEL, + PERF_DIST_LABEL, + PORT_LABEL, + SERVICE_LATENCY_LABEL, + SERVICE_NAME_LABEL, + SERVICE_THROUGHPUT_LABEL, + TAGS_LABEL, + TBT_LABEL, + UPTIME_PINGS_LABEL, + URL_LABEL, +} from './labels'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; +export const RECORDS_FIELD = 'Records'; + export const FieldLabels: Record = { - 'user_agent.name': 'Browser family', - 'user_agent.version': 'Browser version', - 'user_agent.os.name': 'Operating system', - 'client.geo.country_name': 'Location', - 'user_agent.device.name': 'Device', - 'observer.geo.name': 'Observer location', - 'service.name': 'Service Name', - 'service.environment': 'Environment', + 'user_agent.name': BROWSER_FAMILY_LABEL, + 'user_agent.version': BROWSER_VERSION_LABEL, + 'user_agent.os.name': OS_LABEL, + 'client.geo.country_name': LOCATION_LABEL, + 'user_agent.device.name': DEVICE_LABEL, + 'observer.geo.name': OBSERVER_LOCATION_LABEL, + 'service.name': SERVICE_NAME_LABEL, + 'service.environment': ENVIRONMENT_LABEL, - [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', + [LCP_FIELD]: LCP_LABEL, + [FCP_FIELD]: FCP_LABEL, + [TBT_FIELD]: TBT_LABEL, + [FID_FIELD]: FID_LABEL, + [CLS_FIELD]: CLS_LABEL, - 'monitor.id': 'Monitor Id', - 'monitor.status': 'Monitor Status', + 'monitor.id': MONITOR_ID_LABEL, + 'monitor.status': MONITOR_STATUS_LABEL, - 'agent.hostname': 'Agent host', - 'host.hostname': 'Host name', - 'monitor.name': 'Monitor name', - 'monitor.type': 'Monitor Type', - 'url.port': 'Port', - 'url.full': 'URL', - tags: 'Tags', + 'agent.hostname': AGENT_HOST_LABEL, + 'host.hostname': HOST_NAME_LABEL, + 'monitor.name': MONITOR_NAME_LABEL, + 'monitor.type': MONITOR_TYPE_LABEL, + 'url.port': PORT_LABEL, + 'url.full': URL_LABEL, + tags: TAGS_LABEL, // custom - 'performance.metric': 'Metric', - 'Business.KPI': 'KPI', + 'performance.metric': METRIC_LABEL, + 'Business.KPI': KPI_LABEL, }; export const DataViewLabels: Record = { - pld: 'Performance Distribution', - upd: 'Uptime monitor duration', - upp: 'Uptime pings', - svl: 'APM Service latency', - kpi: 'KPI over time', - tpt: 'APM Service throughput', - cpu: 'System CPU Usage', - logs: 'Logs Frequency', - mem: 'System Memory Usage', - nwk: 'Network Activity', + pld: PERF_DIST_LABEL, + upd: MONITOR_DURATION_LABEL, + upp: UPTIME_PINGS_LABEL, + svl: SERVICE_LATENCY_LABEL, + kpi: KIP_OVER_TIME_LABEL, + tpt: SERVICE_THROUGHPUT_LABEL, + cpu: CPU_USAGE_LABEL, + logs: LOGS_FREQUENCY_LABEL, + mem: MEMORY_USAGE_LABEL, + nwk: NETWORK_ACTIVITY_LABEL, }; export const ReportToDataTypeMap: Record = { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts new file mode 100644 index 0000000000000..ba820a25f868a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -0,0 +1,244 @@ +/* + * 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 BROWSER_FAMILY_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.browserFamily', + { + defaultMessage: 'Browser family', + } +); +export const BROWSER_VERSION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.browserVersion', + { + defaultMessage: 'Browser version', + } +); + +export const OS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.os', { + defaultMessage: 'Operating system', +}); +export const LOCATION_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.location', { + defaultMessage: 'Location', +}); + +export const DEVICE_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.device', { + defaultMessage: 'Device', +}); + +export const OBSERVER_LOCATION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.obsLocation', + { + defaultMessage: 'Observer location', + } +); + +export const SERVICE_NAME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.serviceName', + { + defaultMessage: 'Service name', + } +); + +export const ENVIRONMENT_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.environment', + { + defaultMessage: 'Environment', + } +); + +export const LCP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.lcp', { + defaultMessage: 'Largest contentful paint', +}); + +export const FCP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.fcp', { + defaultMessage: 'First contentful paint', +}); + +export const TBT_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.tbt', { + defaultMessage: 'Total blocking time', +}); + +export const FID_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.fid', { + defaultMessage: 'First input delay', +}); + +export const CLS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.cls', { + defaultMessage: 'Cumulative layout shift', +}); + +export const BACKEND_TIME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.backend', + { + defaultMessage: 'Backend time', + } +); + +export const PAGE_LOAD_TIME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.pageLoadTime', + { + defaultMessage: 'Page load time', + } +); + +export const PAGE_VIEWS_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.pageViews', + { + defaultMessage: 'Page views', + } +); + +export const PAGES_LOADED_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.pagesLoaded', + { + defaultMessage: 'Pages loaded', + } +); + +export const MONITOR_ID_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.monitorId', + { + defaultMessage: 'Monitor Id', + } +); + +export const MONITOR_STATUS_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.monitorStatus', + { + defaultMessage: 'Monitor Status', + } +); + +export const AGENT_HOST_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.agentHost', + { + defaultMessage: 'Agent host', + } +); + +export const HOST_NAME_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.hostName', { + defaultMessage: 'Host name', +}); + +export const MONITOR_NAME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.monitorName', + { + defaultMessage: 'Monitor name', + } +); + +export const MONITOR_TYPE_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.monitorType', + { + defaultMessage: 'Monitor type', + } +); + +export const PORT_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.port', { + defaultMessage: 'Port', +}); + +export const URL_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.url', { + defaultMessage: 'URL', +}); + +export const TAGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.tags', { + defaultMessage: 'Tags', +}); + +export const METRIC_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.metric', { + defaultMessage: 'Metric', +}); +export const KPI_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.kpi', { + defaultMessage: 'KPI', +}); + +export const PERF_DIST_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.performanceDistribution', + { + defaultMessage: 'Performance Distribution', + } +); + +export const MONITOR_DURATION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.monitorDuration', + { + defaultMessage: 'Uptime monitor duration', + } +); + +export const UPTIME_PINGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.pings', { + defaultMessage: 'Uptime pings', +}); + +export const SERVICE_LATENCY_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.serviceLatency', + { + defaultMessage: 'APM Service latency', + } +); + +export const SERVICE_THROUGHPUT_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.serviceThroughput', + { + defaultMessage: 'APM Service throughput', + } +); + +export const CPU_USAGE_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.cpuUsage', { + defaultMessage: 'System CPU usage', +}); + +export const NETWORK_ACTIVITY_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.networkActivity', + { + defaultMessage: 'Network activity', + } +); +export const MEMORY_USAGE_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.memoryUsage', + { + defaultMessage: 'System memory usage', + } +); + +export const LOGS_FREQUENCY_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.logsFrequency', + { + defaultMessage: 'Logs frequency', + } +); + +export const KIP_OVER_TIME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.kpiOverTime', + { + defaultMessage: 'KPI over time', + } +); + +export const MONITORS_DURATION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.monitorDurationLabel', + { + defaultMessage: 'Monitor duration', + } +); + +export const WEB_APPLICATION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.webApplication', + { + defaultMessage: 'Web Application', + } +); + +export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.up', { + defaultMessage: 'Up', +}); + +export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.down', { + defaultMessage: 'Down', +}); 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 68d9afc76d51a..a5fdd4971a86f 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 @@ -103,7 +103,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time (Seconds)', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', @@ -125,7 +125,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time (Seconds)', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', @@ -161,7 +161,7 @@ describe('Lens Attribute', () => { expect(lnsAttr.getXAxis()).toEqual({ dataType: 'number', isBucketed: true, - label: 'Page load time (Seconds)', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', @@ -186,7 +186,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time (Seconds)', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', @@ -350,7 +350,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time (Seconds)', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', @@ -395,7 +395,7 @@ describe('Lens Attribute', () => { 'x-axis-column': { dataType: 'number', isBucketed: true, - label: 'Page load time (Seconds)', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', 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 index f656bd764e8b0..029fe5534965e 100644 --- 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 @@ -6,7 +6,7 @@ */ import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels } from '../constants'; +import { FieldLabels, RECORDS_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -27,6 +27,17 @@ import { TRANSACTION_TIME_TO_FIRST_BYTE, TRANSACTION_URL, } from '../constants/elasticsearch_fieldnames'; +import { + BACKEND_TIME_LABEL, + CLS_LABEL, + FCP_LABEL, + FID_LABEL, + LCP_LABEL, + PAGE_LOAD_TIME_LABEL, + PAGE_VIEWS_LABEL, + TBT_LABEL, + WEB_APPLICATION_LABEL, +} from '../constants/labels'; export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -62,7 +73,7 @@ export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], - labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' }, + labels: { ...FieldLabels, [SERVICE_NAME]: WEB_APPLICATION_LABEL }, reportDefinitions: [ { field: SERVICE_NAME, @@ -74,16 +85,20 @@ export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): { field: 'business.kpi', custom: true, - defaultValue: 'Records', + defaultValue: RECORDS_FIELD, 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' }, + { field: RECORDS_FIELD, label: PAGE_VIEWS_LABEL }, + { label: PAGE_LOAD_TIME_LABEL, field: TRANSACTION_DURATION, columnType: 'operation' }, + { + label: BACKEND_TIME_LABEL, + field: TRANSACTION_TIME_TO_FIRST_BYTE, + columnType: 'operation', + }, + { label: FCP_LABEL, field: FCP_FIELD, columnType: 'operation' }, + { label: TBT_LABEL, field: TBT_FIELD, columnType: 'operation' }, + { label: LCP_LABEL, field: LCP_FIELD, columnType: 'operation' }, + { label: FID_LABEL, field: FID_FIELD, columnType: 'operation' }, + { label: CLS_LABEL, field: CLS_FIELD, columnType: 'operation' }, ], }, ], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts index 85380241b63b2..af8bd00a69553 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels } from '../constants'; +import { FieldLabels, RECORDS_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -27,6 +27,17 @@ import { USER_AGENT_OS, USER_AGENT_VERSION, } from '../constants/elasticsearch_fieldnames'; +import { + BACKEND_TIME_LABEL, + CLS_LABEL, + FCP_LABEL, + FID_LABEL, + LCP_LABEL, + PAGE_LOAD_TIME_LABEL, + PAGES_LOADED_LABEL, + TBT_LABEL, + WEB_APPLICATION_LABEL, +} from '../constants/labels'; export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -39,8 +50,8 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP }, yAxisColumns: [ { - sourceField: 'Records', - label: 'Pages loaded', + sourceField: RECORDS_FIELD, + label: PAGES_LOADED_LABEL, }, ], hasOperationType: false, @@ -71,14 +82,13 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP custom: true, 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 - { label: 'Largest contentful paint', field: LCP_FIELD, description: 'Core web vital' }, - { label: 'First input delay', field: FID_FIELD, description: 'Core web vital' }, - { label: 'Cumulative layout shift', field: CLS_FIELD, description: 'Core web vital' }, + { label: PAGE_LOAD_TIME_LABEL, field: TRANSACTION_DURATION }, + { label: BACKEND_TIME_LABEL, field: TRANSACTION_TIME_TO_FIRST_BYTE }, + { label: FCP_LABEL, field: FCP_FIELD }, + { label: TBT_LABEL, field: TBT_FIELD }, + { label: LCP_LABEL, field: LCP_FIELD }, + { label: FID_LABEL, field: FID_FIELD }, + { label: CLS_LABEL, field: CLS_FIELD }, ], }, ], @@ -88,8 +98,8 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP ], labels: { ...FieldLabels, - [SERVICE_NAME]: 'Web Application', - [TRANSACTION_DURATION]: 'Page load time (Seconds)', + [SERVICE_NAME]: WEB_APPLICATION_LABEL, + [TRANSACTION_DURATION]: PAGE_LOAD_TIME_LABEL, }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts index a191a6de4f89a..698b8f9e951e1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts @@ -8,6 +8,7 @@ import { ConfigProps, DataSeries } from '../../types'; import { FieldLabels } from '../constants'; import { buildExistsFilter } from '../utils'; +import { MONITORS_DURATION_LABEL } from '../constants/labels'; export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -22,7 +23,7 @@ export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps { operationType: 'average', sourceField: 'monitor.duration.us', - label: 'Monitor duration (ms)', + label: MONITORS_DURATION_LABEL, }, ], hasOperationType: true, @@ -44,6 +45,6 @@ export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps field: 'url.full', }, ], - labels: { ...FieldLabels, 'monitor.duration.us': 'Monitor duration' }, + labels: { ...FieldLabels, 'monitor.duration.us': MONITORS_DURATION_LABEL }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts index 400ef960b1f68..fc33c37c7bcad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts @@ -8,6 +8,7 @@ import { ConfigProps, DataSeries } from '../../types'; import { FieldLabels } from '../constants'; import { buildExistsFilter } from '../utils'; +import { DOWN_LABEL, UP_LABEL } from '../constants/labels'; export function getMonitorPingsConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { return { @@ -22,12 +23,12 @@ export function getMonitorPingsConfig({ seriesId, indexPattern }: ConfigProps): { operationType: 'sum', sourceField: 'summary.up', - label: 'Up', + label: UP_LABEL, }, { operationType: 'sum', sourceField: 'summary.down', - label: 'Down', + label: DOWN_LABEL, }, ], yTitle: 'Pings', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index ffce81207472f..9b299e7d70bcc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_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 (Seconds)', + label: 'Page load time', dataType: 'number', operationType: 'range', isBucketed: true, 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 index d3c4cee6d7dc1..3943ae3710209 100644 --- 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 @@ -14,6 +14,10 @@ import { useFetcher } from '../../../../..'; import { useUrlStorage } from '../../hooks/use_url_storage'; import { SeriesType } from '../../../../../../../lens/public'; +const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', { + defaultMessage: 'Chart type', +}); + export function SeriesChartTypesSelect({ seriesId, defaultChartType, @@ -38,9 +42,7 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} - label={i18n.translate('xpack.observability.expView.chartTypes.label', { - defaultMessage: 'Chart type', - })} + label={CHART_TYPE_LABEL} includeChartTypes={['bar', 'bar_horizontal', 'line', 'area', 'bar_stacked', 'area_stacked']} /> ); @@ -96,7 +98,7 @@ export function XYChartTypesSelect({ { + public async screenshot(elementPosition: ElementPosition): Promise { const { boundingClientRect, scroll } = elementPosition; const screenshot = await this.page.screenshot({ clip: { @@ -138,7 +138,10 @@ export class HeadlessChromiumDriver { }, }); - return screenshot.toString('base64'); + if (screenshot) { + return screenshot.toString('base64'); + } + return screenshot; } public async evaluate( @@ -160,6 +163,11 @@ export class HeadlessChromiumDriver { const { timeout } = opts; logger.debug(`waitForSelector ${selector}`); const resp = await this.page.waitForSelector(selector, { timeout }); // override default 30000ms + + if (!resp) { + throw new Error(`Failure in waitForSelector: void response! Context: ${context.context}`); + } + logger.debug(`waitForSelector ${selector} resolved`); return resp; } @@ -219,6 +227,7 @@ export class HeadlessChromiumDriver { } // @ts-ignore + // FIXME: use `await page.target().createCDPSession();` const client = this.page._client; // We have to reach into the Chrome Devtools Protocol to apply headers as using @@ -293,7 +302,7 @@ export class HeadlessChromiumDriver { // Even though 3xx redirects go through our request // handler, we should probably inspect responses just to // avoid being bamboozled by some malicious request - this.page.on('response', (interceptedResponse: Response) => { + this.page.on('response', (interceptedResponse: puppeteer.Response) => { const interceptedUrl = interceptedResponse.url(); const allowed = !interceptedUrl.startsWith('file://'); @@ -315,17 +324,17 @@ export class HeadlessChromiumDriver { private async launchDebugger() { // In order to pause on execution we have to reach more deeply into Chromiums Devtools Protocol, - // and more specifically, for the page being used. _client is per-page, and puppeteer doesn't expose - // a page's client in their api, so we have to reach into internals to get this behavior. - // Finally, in order to get the inspector running, we have to know the page's internal ID (again, private) + // and more specifically, for the page being used. _client is per-page. + // In order to get the inspector running, we have to know the page's internal ID (again, private) // in order to construct the final debugging URL. + const target = this.page.target(); + const client = await target.createCDPSession(); + + await client.send('Debugger.enable'); + await client.send('Debugger.pause'); // @ts-ignore - await this.page._client.send('Debugger.enable'); - // @ts-ignore - await this.page._client.send('Debugger.pause'); - // @ts-ignore - const targetId = this.page._target._targetId; + const targetId = target._targetId; const wsEndpoint = this.page.browser().wsEndpoint(); const { port } = parseUrl(wsEndpoint); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index fdeb2e5cb3831..94f0db394d166 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -193,6 +193,10 @@ export class HeadlessChromiumDriverFactory { // Puppeteer doesn't give a handle to the original ChildProcess object // See https://github.com/GoogleChrome/puppeteer/issues/1292#issuecomment-521470627 + if (childProcess == null) { + throw new TypeError('childProcess is null or undefined!'); + } + // just log closing of the process const processClose$ = Rx.fromEvent(childProcess, 'close').pipe( tap(() => { diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts index 617a873a147df..cfb3abeda1e1a 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts @@ -10,16 +10,17 @@ import { spawn } from 'child_process'; import del from 'del'; import { mkdtempSync } from 'fs'; import { uniq } from 'lodash'; -import { tmpdir } from 'os'; +import os, { tmpdir } from 'os'; import { join } from 'path'; import { createInterface } from 'readline'; -import { fromEvent, timer, merge, of } from 'rxjs'; -import { takeUntil, map, reduce, tap, catchError } from 'rxjs/operators'; -import { ReportingCore } from '../../..'; +import { fromEvent, merge, of, timer } from 'rxjs'; +import { catchError, map, reduce, takeUntil, tap } from 'rxjs/operators'; +import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; -import { getBinaryPath } from '../../install'; +import { ChromiumArchivePaths } from '../paths'; import { args } from './args'; +const paths = new ChromiumArchivePaths(); const browserLaunchTimeToWait = 5 * 1000; // Default args used by pptr @@ -61,7 +62,15 @@ export const browserStartLogs = ( const proxy = config.get('capture', 'browser', 'chromium', 'proxy'); const disableSandbox = config.get('capture', 'browser', 'chromium', 'disableSandbox'); const userDataDir = mkdtempSync(join(tmpdir(), 'chromium-')); - const binaryPath = getBinaryPath(); + + const platform = process.platform; + const architecture = os.arch(); + const pkg = paths.find(platform, architecture); + if (!pkg) { + throw new Error(`Unsupported platform: ${platform}-${architecture}`); + } + const binaryPath = paths.getBinaryPath(pkg); + const kbnArgs = args({ userDataDir, viewport: { width: 800, height: 600 }, diff --git a/x-pack/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/index.ts index fe4c67eb32404..0d5639254b816 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/index.ts @@ -10,10 +10,10 @@ import { BrowserDownload } from '../'; import { CaptureConfig } from '../../../server/types'; import { LevelLogger } from '../../lib'; import { HeadlessChromiumDriverFactory } from './driver_factory'; -import { paths } from './paths'; +import { ChromiumArchivePaths } from './paths'; export const chromium: BrowserDownload = { - paths, + paths: new ChromiumArchivePaths(), createDriverFactory: (binaryPath: string, captureConfig: CaptureConfig, logger: LevelLogger) => new HeadlessChromiumDriverFactory(binaryPath, captureConfig, logger), }; @@ -32,3 +32,5 @@ export const getDisallowedOutgoingUrlError = (interceptedUrl: string) => values: { interceptedUrl }, }) ); + +export { ChromiumArchivePaths }; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/paths.ts b/x-pack/plugins/reporting/server/browsers/chromium/paths.ts index 37057735fed10..8a02a97944ecc 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/paths.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/paths.ts @@ -7,12 +7,24 @@ import path from 'path'; -export const paths = { - archivesPath: path.resolve(__dirname, '../../../../../../.chromium'), - baseUrl: 'https://storage.googleapis.com/headless_shell/', - packages: [ +interface PackageInfo { + platform: string; + architecture: string; + archiveFilename: string; + archiveChecksum: string; + binaryChecksum: string; + binaryRelativePath: string; +} + +// We download zip files from a Kibana team GCS bucket named `headless_shell` +enum BaseUrl { + custom = 'https://storage.googleapis.com/headless_shell', +} + +export class ChromiumArchivePaths { + public readonly packages: PackageInfo[] = [ { - platforms: ['darwin', 'freebsd', 'openbsd'], + platform: 'darwin', architecture: 'x64', archiveFilename: 'chromium-ef768c9-darwin_x64.zip', archiveChecksum: 'd87287f6b2159cff7c64babac873cc73', @@ -20,7 +32,7 @@ export const paths = { binaryRelativePath: 'headless_shell-darwin_x64/headless_shell', }, { - platforms: ['linux'], + platform: 'linux', architecture: 'x64', archiveFilename: 'chromium-ef768c9-linux_x64.zip', archiveChecksum: '85575e8fd56849f4de5e3584e05712c0', @@ -28,7 +40,7 @@ export const paths = { binaryRelativePath: 'headless_shell-linux_x64/headless_shell', }, { - platforms: ['linux'], + platform: 'linux', architecture: 'arm64', archiveFilename: 'chromium-ef768c9-linux_arm64.zip', archiveChecksum: '20b09b70476bea76a276c583bf72eac7', @@ -36,12 +48,36 @@ export const paths = { binaryRelativePath: 'headless_shell-linux_arm64/headless_shell', }, { - platforms: ['win32'], + platform: 'win32', architecture: 'x64', archiveFilename: 'chromium-ef768c9-windows_x64.zip', archiveChecksum: '33301c749b5305b65311742578c52f15', binaryChecksum: '9f28dd56c7a304a22bf66f0097fa4de9', binaryRelativePath: 'headless_shell-windows_x64\\headless_shell.exe', }, - ], -}; + ]; + + // zip files get downloaded to a .chromium directory in the kibana root + public readonly archivesPath = path.resolve(__dirname, '../../../../../../.chromium'); + + public find(platform: string, architecture: string) { + return this.packages.find((p) => p.platform === platform && p.architecture === architecture); + } + + public resolvePath(p: PackageInfo) { + return path.resolve(this.archivesPath, p.archiveFilename); + } + + public getAllArchiveFilenames(): string[] { + return this.packages.map((p) => this.resolvePath(p)); + } + + public getDownloadUrl(p: PackageInfo) { + return BaseUrl.custom + `/${p.archiveFilename}`; + } + + public getBinaryPath(p: PackageInfo) { + const chromiumPath = path.resolve(__dirname, '../../../chromium'); + return path.join(chromiumPath, p.binaryRelativePath); + } +} diff --git a/x-pack/plugins/reporting/server/browsers/download/checksum.ts b/x-pack/plugins/reporting/server/browsers/download/checksum.ts index 24c77b8123b4a..35feb1ff534ab 100644 --- a/x-pack/plugins/reporting/server/browsers/download/checksum.ts +++ b/x-pack/plugins/reporting/server/browsers/download/checksum.ts @@ -7,8 +7,13 @@ import { createHash } from 'crypto'; import { createReadStream } from 'fs'; +import { Readable } from 'stream'; -import { readableEnd } from './util'; +function readableEnd(stream: Readable) { + return new Promise((resolve, reject) => { + stream.on('error', reject).on('end', resolve); + }); +} export async function md5(path: string) { const hash = createHash('md5'); diff --git a/x-pack/plugins/reporting/server/browsers/download/clean.ts b/x-pack/plugins/reporting/server/browsers/download/clean.ts index 633db0545a2ab..1f8e798d30669 100644 --- a/x-pack/plugins/reporting/server/browsers/download/clean.ts +++ b/x-pack/plugins/reporting/server/browsers/download/clean.ts @@ -9,7 +9,6 @@ import del from 'del'; import { readdirSync } from 'fs'; import { resolve as resolvePath } from 'path'; import { GenericLevelLogger } from '../../lib/level_logger'; -import { asyncMap } from './util'; /** * Delete any file in the `dir` that is not in the expectedPaths @@ -17,7 +16,7 @@ import { asyncMap } from './util'; export async function clean(dir: string, expectedPaths: string[], logger: GenericLevelLogger) { let filenames: string[]; try { - filenames = await readdirSync(dir); + filenames = readdirSync(dir); } catch (error) { if (error.code === 'ENOENT') { // directory doesn't exist, that's as clean as it gets @@ -27,11 +26,13 @@ export async function clean(dir: string, expectedPaths: string[], logger: Generi throw error; } - await asyncMap(filenames, async (filename) => { - const path = resolvePath(dir, filename); - if (!expectedPaths.includes(path)) { - logger.warning(`Deleting unexpected file ${path}`); - await del(path, { force: true }); - } - }); + await Promise.all( + filenames.map(async (filename) => { + const path = resolvePath(dir, filename); + if (!expectedPaths.includes(path)) { + logger.warning(`Deleting unexpected file ${path}`); + await del(path, { force: true }); + } + }) + ); } diff --git a/x-pack/plugins/reporting/server/browsers/download/download.ts b/x-pack/plugins/reporting/server/browsers/download/download.ts index c4ec51522dfc5..77efc75ae1aaa 100644 --- a/x-pack/plugins/reporting/server/browsers/download/download.ts +++ b/x-pack/plugins/reporting/server/browsers/download/download.ts @@ -13,11 +13,12 @@ import { GenericLevelLogger } from '../../lib/level_logger'; /** * Download a url and calculate it's checksum - * @param {String} url - * @param {String} path - * @return {Promise} checksum of the downloaded file */ -export async function download(url: string, path: string, logger: GenericLevelLogger) { +export async function download( + url: string, + path: string, + logger: GenericLevelLogger +): Promise { logger.info(`Downloading ${url} to ${path}`); const hash = createHash('md5'); diff --git a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts index 5dbd9beec6a62..38e546166aef5 100644 --- a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts +++ b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts @@ -6,13 +6,11 @@ */ import { existsSync } from 'fs'; -import { resolve as resolvePath } from 'path'; import { BrowserDownload, chromium } from '../'; import { GenericLevelLogger } from '../../lib/level_logger'; import { md5 } from './checksum'; import { clean } from './clean'; import { download } from './download'; -import { asyncMap } from './util'; /** * Check for the downloaded archive of each requested browser type and @@ -31,39 +29,46 @@ export async function ensureBrowserDownloaded(logger: GenericLevelLogger) { * @return {Promise} */ async function ensureDownloaded(browsers: BrowserDownload[], logger: GenericLevelLogger) { - await asyncMap(browsers, async (browser) => { - const { archivesPath } = browser.paths; + await Promise.all( + browsers.map(async ({ paths: pSet }) => { + await clean(pSet.archivesPath, pSet.getAllArchiveFilenames(), logger); - await clean( - archivesPath, - browser.paths.packages.map((p) => resolvePath(archivesPath, p.archiveFilename)), - logger - ); + const invalidChecksums: string[] = []; + await Promise.all( + pSet.packages.map(async (p) => { + const { archiveFilename, archiveChecksum } = p; + if (archiveFilename && archiveChecksum) { + const path = pSet.resolvePath(p); - const invalidChecksums: string[] = []; - await asyncMap(browser.paths.packages, async ({ archiveFilename, archiveChecksum }) => { - const url = `${browser.paths.baseUrl}${archiveFilename}`; - const path = resolvePath(archivesPath, archiveFilename); + if (existsSync(path) && (await md5(path)) === archiveChecksum) { + logger.debug(`Browser archive exists in ${path}`); + return; + } - if (existsSync(path) && (await md5(path)) === archiveChecksum) { - logger.debug(`Browser archive exists in ${path}`); - return; - } + const url = pSet.getDownloadUrl(p); + try { + const downloadedChecksum = await download(url, path, logger); + if (downloadedChecksum !== archiveChecksum) { + invalidChecksums.push(`${url} => ${path}`); + } + } catch (err) { + const message = new Error(`Failed to download ${url}`); + logger.error(err); + throw message; + } + } + }) + ); - const downloadedChecksum = await download(url, path, logger); - if (downloadedChecksum !== archiveChecksum) { - invalidChecksums.push(`${url} => ${path}`); + if (invalidChecksums.length) { + const err = new Error( + `Error downloading browsers, checksums incorrect for:\n - ${invalidChecksums.join( + '\n - ' + )}` + ); + logger.error(err); + throw err; } - }); - - if (invalidChecksums.length) { - const err = new Error( - `Error downloading browsers, checksums incorrect for:\n - ${invalidChecksums.join( - '\n - ' - )}` - ); - logger.error(err); - throw err; - } - }); + }) + ); } diff --git a/x-pack/plugins/reporting/server/browsers/download/util.ts b/x-pack/plugins/reporting/server/browsers/download/util.ts deleted file mode 100644 index a4e90b7f0df55..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/download/util.ts +++ /dev/null @@ -1,24 +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 { Readable } from 'stream'; - -/** - * Iterate an array asynchronously and in parallel - */ -export function asyncMap(array: T[], asyncFn: (x: T) => T2): Promise { - return Promise.all(array.map(asyncFn)); -} - -/** - * Wait for a readable stream to end - */ -export function readableEnd(stream: Readable) { - return new Promise((resolve, reject) => { - stream.on('error', reject).on('end', resolve); - }); -} diff --git a/x-pack/plugins/reporting/server/browsers/extract/extract.test.js b/x-pack/plugins/reporting/server/browsers/extract/extract.test.ts similarity index 86% rename from x-pack/plugins/reporting/server/browsers/extract/extract.test.js rename to x-pack/plugins/reporting/server/browsers/extract/extract.test.ts index 0253f39aae677..fe631c4b4e67f 100644 --- a/x-pack/plugins/reporting/server/browsers/extract/extract.test.js +++ b/x-pack/plugins/reporting/server/browsers/extract/extract.test.ts @@ -7,17 +7,17 @@ import fs from 'fs'; import crypto from 'crypto'; -import { resolve } from 'path'; +import { resolve as pathResolve } from 'path'; import { extract } from './extract'; import { ExtractError } from './extract_error'; import { promisify } from 'util'; -const FIXTURES_FOLDER = resolve(__dirname, '__fixtures__'); -const SRC_FILE_UNCOMPRESSED = resolve(FIXTURES_FOLDER, 'file.md'); +const FIXTURES_FOLDER = pathResolve(__dirname, '__fixtures__'); +const SRC_FILE_UNCOMPRESSED = pathResolve(FIXTURES_FOLDER, 'file.md'); const SRC_FILE_COMPRESSED_ZIP = `${SRC_FILE_UNCOMPRESSED}.zip`; -const EXTRACT_TARGET_FOLDER = resolve(FIXTURES_FOLDER, 'extract_target'); -const EXTRACT_TARGET_FILE = resolve(EXTRACT_TARGET_FOLDER, 'file.md'); +const EXTRACT_TARGET_FOLDER = pathResolve(FIXTURES_FOLDER, 'extract_target'); +const EXTRACT_TARGET_FILE = pathResolve(EXTRACT_TARGET_FOLDER, 'file.md'); const fsp = { mkdir: promisify(fs.mkdir), @@ -25,7 +25,7 @@ const fsp = { unlink: promisify(fs.unlink), }; -const ignoreErrorCodes = async (codes, promise) => { +const ignoreErrorCodes = async (codes: string[], promise: Promise) => { try { await promise; } catch (err) { @@ -40,7 +40,7 @@ async function cleanup() { await ignoreErrorCodes(['ENOENT'], fsp.rmdir(EXTRACT_TARGET_FOLDER)); } -function fileHash(filepath) { +function fileHash(filepath: string) { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha256'); const input = fs.createReadStream(filepath); diff --git a/x-pack/plugins/reporting/server/browsers/extract/extract.js b/x-pack/plugins/reporting/server/browsers/extract/extract.ts similarity index 89% rename from x-pack/plugins/reporting/server/browsers/extract/extract.js rename to x-pack/plugins/reporting/server/browsers/extract/extract.ts index 8af7f78d1365b..ccdfb1eaad5c2 100644 --- a/x-pack/plugins/reporting/server/browsers/extract/extract.js +++ b/x-pack/plugins/reporting/server/browsers/extract/extract.ts @@ -9,7 +9,7 @@ import path from 'path'; import { unzip } from './unzip'; import { ExtractError } from './extract_error'; -export async function extract(archivePath, targetPath) { +export async function extract(archivePath: string, targetPath: string) { const fileType = path.parse(archivePath).ext.substr(1); let unpacker; diff --git a/x-pack/plugins/reporting/server/browsers/extract/extract_error.js b/x-pack/plugins/reporting/server/browsers/extract/extract_error.ts similarity index 80% rename from x-pack/plugins/reporting/server/browsers/extract/extract_error.js rename to x-pack/plugins/reporting/server/browsers/extract/extract_error.ts index e3516003f986a..838b8a7dbc158 100644 --- a/x-pack/plugins/reporting/server/browsers/extract/extract_error.js +++ b/x-pack/plugins/reporting/server/browsers/extract/extract_error.ts @@ -6,7 +6,8 @@ */ export class ExtractError extends Error { - constructor(cause, message = 'Failed to extract the browser archive') { + public readonly cause: string; + constructor(cause: string, message = 'Failed to extract the browser archive') { super(message); this.message = message; this.name = this.constructor.name; diff --git a/x-pack/plugins/reporting/server/browsers/extract/index.js b/x-pack/plugins/reporting/server/browsers/extract/index.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/index.js rename to x-pack/plugins/reporting/server/browsers/extract/index.ts diff --git a/x-pack/plugins/reporting/server/browsers/extract/unzip.js b/x-pack/plugins/reporting/server/browsers/extract/unzip.ts similarity index 87% rename from x-pack/plugins/reporting/server/browsers/extract/unzip.js rename to x-pack/plugins/reporting/server/browsers/extract/unzip.ts index 95ae48b28e811..8b5f381dad84a 100644 --- a/x-pack/plugins/reporting/server/browsers/extract/unzip.js +++ b/x-pack/plugins/reporting/server/browsers/extract/unzip.ts @@ -8,7 +8,7 @@ import extractZip from 'extract-zip'; import { ExtractError } from './extract_error'; -export async function unzip(filepath, target) { +export async function unzip(filepath: string, target: string) { try { await extractZip(filepath, { dir: target }); } catch (err) { diff --git a/x-pack/plugins/reporting/server/browsers/index.ts b/x-pack/plugins/reporting/server/browsers/index.ts index 96b994e522eea..df95b69d9d254 100644 --- a/x-pack/plugins/reporting/server/browsers/index.ts +++ b/x-pack/plugins/reporting/server/browsers/index.ts @@ -6,16 +6,16 @@ */ import { first } from 'rxjs/operators'; +import { ReportingConfig } from '../'; import { LevelLogger } from '../lib'; import { CaptureConfig } from '../types'; -import { chromium } from './chromium'; +import { chromium, ChromiumArchivePaths } from './chromium'; import { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; import { installBrowser } from './install'; -import { ReportingConfig } from '..'; +export { chromium } from './chromium'; export { HeadlessChromiumDriver } from './chromium/driver'; export { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; -export { chromium } from './chromium'; type CreateDriverFactory = ( binaryPath: string, @@ -25,17 +25,7 @@ type CreateDriverFactory = ( export interface BrowserDownload { createDriverFactory: CreateDriverFactory; - paths: { - archivesPath: string; - baseUrl: string; - packages: Array<{ - archiveChecksum: string; - archiveFilename: string; - binaryChecksum: string; - binaryRelativePath: string; - platforms: string[]; - }>; - }; + paths: ChromiumArchivePaths; } export const initializeBrowserDriverFactory = async ( diff --git a/x-pack/plugins/reporting/server/browsers/install.ts b/x-pack/plugins/reporting/server/browsers/install.ts index 71b87f4eaf1ea..93d860d0528fe 100644 --- a/x-pack/plugins/reporting/server/browsers/install.ts +++ b/x-pack/plugins/reporting/server/browsers/install.ts @@ -10,38 +10,11 @@ import os from 'os'; import path from 'path'; import * as Rx from 'rxjs'; import { GenericLevelLogger } from '../lib/level_logger'; -import { paths } from './chromium/paths'; +import { ChromiumArchivePaths } from './chromium'; import { ensureBrowserDownloaded } from './download'; -// @ts-ignore import { md5 } from './download/checksum'; -// @ts-ignore import { extract } from './extract'; -interface Package { - platforms: string[]; - architecture: string; -} - -/** - * Small helper util to resolve where chromium is installed - */ -export const getBinaryPath = ( - chromiumPath: string = path.resolve(__dirname, '../../chromium'), - platform: string = process.platform, - architecture: string = os.arch() -) => { - const pkg = paths.packages.find((p: Package) => { - return p.platforms.includes(platform) && p.architecture === architecture; - }); - - if (!pkg) { - // TODO: validate this - throw new Error(`Unsupported platform: ${platform}-${architecture}`); - } - - return path.join(chromiumPath, pkg.binaryRelativePath); -}; - /** * "install" a browser by type into installs path by extracting the downloaded * archive. If there is an error extracting the archive an `ExtractError` is thrown @@ -53,17 +26,16 @@ export function installBrowser( architecture: string = os.arch() ): { binaryPath$: Rx.Subject } { const binaryPath$ = new Rx.Subject(); - const backgroundInstall = async () => { - const pkg = paths.packages.find((p: Package) => { - return p.platforms.includes(platform) && p.architecture === architecture; - }); - if (!pkg) { - // TODO: validate this - throw new Error(`Unsupported platform: ${platform}-${architecture}`); - } + const paths = new ChromiumArchivePaths(); + const pkg = paths.find(platform, architecture); - const binaryPath = getBinaryPath(chromiumPath, platform, architecture); + if (!pkg) { + throw new Error(`Unsupported platform: ${platform}-${architecture}`); + } + + const backgroundInstall = async () => { + const binaryPath = paths.getBinaryPath(pkg); const binaryChecksum = await md5(binaryPath).catch(() => ''); if (binaryChecksum !== pkg.binaryChecksum) { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts index 15d4b1d7476bc..b279fe5f082ee 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts @@ -80,6 +80,10 @@ export const getScreenshots = async ( await resizeToClipArea(item, browser, layout.getBrowserZoom(), logger); const base64EncodedData = await browser.screenshot(item.position); + if (!base64EncodedData) { + throw new Error(`Failure in getScreenshots! Base64 data is void`); + } + screenshots.push({ base64EncodedData, title: item.attributes.title, diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts index af45314c5bacb..97bbd0848e9c4 100644 --- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts +++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts @@ -8,7 +8,6 @@ import type { ApplicationSetup, FatalErrorsSetup, HttpSetup } from 'src/core/public'; import { AUTH_URL_HASH_QUERY_STRING_PARAMETER } from '../../../common/constants'; -import { parseNext } from '../../../common/parse_next'; interface CreateDeps { application: ApplicationSetup; @@ -46,6 +45,9 @@ export const captureURLApp = Object.freeze({ appRoute: '/internal/security/capture-url', async mount() { try { + // This is an async import because it requires `url`, which is a sizable dependency. + // Otherwise this becomes part of the "page load bundle". + const { parseNext } = await import('../../../common/parse_next'); const url = new URL( parseNext(window.location.href, http.basePath.serverBasePath), window.location.origin diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index d224edb8cafd4..1faa105691259 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -7,7 +7,7 @@ import BroadcastChannel from 'broadcast-channel'; -import { mountWithIntl } from '@kbn/test/jest'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { coreMock } from 'src/core/public/mocks'; import { createSessionExpiredMock } from './session_expired.mock'; @@ -112,6 +112,7 @@ describe('Session Timeout', () => { afterEach(async () => { jest.clearAllMocks(); + jest.unmock('broadcast-channel'); sessionTimeout.stop(); }); @@ -122,22 +123,42 @@ describe('Session Timeout', () => { describe('Lifecycle', () => { test(`starts and initializes on a non-anonymous path`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // eslint-disable-next-line dot-notation expect(sessionTimeout['channel']).not.toBeUndefined(); expect(http.fetch).toHaveBeenCalledTimes(1); }); + test(`starts and initializes if the broadcast channel fails to load`, async () => { + jest.mock('broadcast-channel', () => { + throw new Error('Unable to load broadcast channel!'); + }); + const consoleSpy = jest.spyOn(console, 'warn'); + + sessionTimeout.start(); + await nextTick(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).toBeUndefined(); + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy.mock.calls[0][0]).toMatchInlineSnapshot( + `"Failed to load broadcast channel. Session management will not be kept in sync when multiple tabs are loaded."` + ); + }); + test(`starts and does not initialize on an anonymous path`, async () => { http.anonymousPaths.isAnonymous.mockReturnValue(true); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // eslint-disable-next-line dot-notation expect(sessionTimeout['channel']).toBeUndefined(); expect(http.fetch).not.toHaveBeenCalled(); }); test(`stops`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // eslint-disable-next-line dot-notation const close = jest.fn(sessionTimeout['channel']!.close); // eslint-disable-next-line dot-notation @@ -157,7 +178,8 @@ describe('Session Timeout', () => { ...defaultSessionInfo, idleTimeoutExpiration: now + 5_000_000_000, }); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // Advance timers far enough to call intermediate `setTimeout` multiple times, but before any // of the timers is supposed to be triggered. @@ -184,7 +206,8 @@ describe('Session Timeout', () => { }); test(`handles success`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line dot-notation @@ -195,7 +218,8 @@ describe('Session Timeout', () => { test(`handles error`, async () => { const mockErrorResponse = new Error('some-error'); http.fetch.mockRejectedValue(mockErrorResponse); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line dot-notation @@ -206,7 +230,8 @@ describe('Session Timeout', () => { describe('warning toast', () => { test(`shows idle timeout warning toast`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires jest.advanceTimersByTime(55 * 1000); @@ -218,7 +243,8 @@ describe('Session Timeout', () => { ...defaultSessionInfo, idleTimeoutExpiration: now + 5_000_000_000, }); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires jest.advanceTimersByTime(5_000_000_000 - 66 * 1000); @@ -236,7 +262,8 @@ describe('Session Timeout', () => { provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires jest.advanceTimersByTime(55 * 1000); @@ -250,7 +277,8 @@ describe('Session Timeout', () => { lifespanExpiration: now + 5_000_000_000, }; http.fetch.mockResolvedValue(sessionInfo); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires jest.advanceTimersByTime(5_000_000_000 - 66 * 1000); @@ -261,7 +289,8 @@ describe('Session Timeout', () => { }); test(`extend only results in an HTTP call if a warning is shown`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); await sessionTimeout.extend('/foo'); @@ -287,7 +316,8 @@ describe('Session Timeout', () => { provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires jest.advanceTimersByTime(55 * 1000); @@ -299,7 +329,8 @@ describe('Session Timeout', () => { }); test(`extend hides displayed warning toast`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires @@ -319,7 +350,8 @@ describe('Session Timeout', () => { }); test(`extend does nothing for session-related routes`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires @@ -333,7 +365,8 @@ describe('Session Timeout', () => { }); test(`checks for updated session info before the warning displays`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); // we check for updated session info 1 second before the warning is shown @@ -343,7 +376,8 @@ describe('Session Timeout', () => { }); test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires @@ -366,7 +400,8 @@ describe('Session Timeout', () => { lifespanExpiration: null, provider: { type: 'basic', name: 'basic1' }, }); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalled(); jest.advanceTimersByTime(0); @@ -376,7 +411,8 @@ describe('Session Timeout', () => { describe('session expiration', () => { test(`expires the session 5 seconds before it really expires`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); jest.advanceTimersByTime(114 * 1000); expect(sessionExpired.logout).not.toHaveBeenCalled(); @@ -391,7 +427,8 @@ describe('Session Timeout', () => { idleTimeoutExpiration: now + 5_000_000_000, }); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); jest.advanceTimersByTime(5_000_000_000 - 6000); expect(sessionExpired.logout).not.toHaveBeenCalled(); @@ -401,7 +438,8 @@ describe('Session Timeout', () => { }); test(`extend delays the expiration`, async () => { - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); expect(http.fetch).toHaveBeenCalledTimes(1); const elapsed = 114 * 1000; @@ -438,7 +476,8 @@ describe('Session Timeout', () => { lifespanExpiration: null, provider: { type: 'basic', name: 'basic1' }, }); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); jest.advanceTimersByTime(0); expect(sessionExpired.logout).toHaveBeenCalled(); @@ -446,7 +485,8 @@ describe('Session Timeout', () => { test(`'null' sessionTimeout never logs you out`, async () => { http.fetch.mockResolvedValue({ now, idleTimeoutExpiration: null, lifespanExpiration: null }); - await sessionTimeout.start(); + sessionTimeout.start(); + await nextTick(); jest.advanceTimersByTime(Number.MAX_VALUE); expect(sessionExpired.logout).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index cc7eaa551b1b3..2288fce8d30af 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { BroadcastChannel } from 'broadcast-channel'; +import type { BroadcastChannel as BroadcastChannelType } from 'broadcast-channel'; import type { HttpSetup, NotificationsSetup, Toast, ToastInput } from 'src/core/public'; @@ -45,7 +45,7 @@ export interface ISessionTimeout { } export class SessionTimeout implements ISessionTimeout { - private channel?: BroadcastChannel; + private channel?: BroadcastChannelType; private sessionInfo?: SessionInfo; private fetchTimer?: number; private warningTimer?: number; @@ -64,15 +64,26 @@ export class SessionTimeout implements ISessionTimeout { return; } - // subscribe to a broadcast channel for session timeout messages - // this allows us to synchronize the UX across tabs and avoid repetitive API calls - const name = `${this.tenant}/session_timeout`; - this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); - this.channel.onmessage = this.handleSessionInfoAndResetTimers; - - // Triggers an initial call to the endpoint to get session info; - // when that returns, it will set the timeout - return this.fetchSessionInfoAndResetTimers(); + import('broadcast-channel') + .then(({ BroadcastChannel }) => { + // subscribe to a broadcast channel for session timeout messages + // this allows us to synchronize the UX across tabs and avoid repetitive API calls + const name = `${this.tenant}/session_timeout`; + this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); + this.channel.onmessage = this.handleSessionInfoAndResetTimers; + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.warn( + `Failed to load broadcast channel. Session management will not be kept in sync when multiple tabs are loaded.`, + e + ); + }) + .finally(() => { + // Triggers an initial call to the endpoint to get session info; + // when that returns, it will set the timeout + return this.fetchSessionInfoAndResetTimers(); + }); } stop() { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index a735f3885cf2c..fa6bb497d2434 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { TransformConfigSchema } from './transforms/types'; import { ENABLE_CASE_CONNECTOR } from '../../cases/common'; export const APP_ID = 'securitySolution'; @@ -38,6 +39,7 @@ export const DEFAULT_INTERVAL_PAUSE = true; export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; +export const DEFAULT_TRANSFORMS = 'securitySolution:transforms'; export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled'; export const GLOBAL_HEADER_HEIGHT = 98; // px export const FILTERS_GLOBAL_HEIGHT = 109; // px @@ -106,6 +108,38 @@ export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[ { "name": "talosIntelligence.com", "url_template": "https://talosintelligence.com/reputation_center/lookup?search={{ip}}" } ]`; +/** The default settings for the transforms */ +export const defaultTransformsSetting: TransformConfigSchema = { + enabled: false, + auto_start: true, + auto_create: true, + query: { + range: { + '@timestamp': { + gte: 'now-1d/d', + format: 'strict_date_optional_time', + }, + }, + }, + retention_policy: { + time: { + field: '@timestamp', + max_age: '1w', + }, + }, + max_page_search_size: 5000, + settings: [ + { + prefix: 'all', + indices: ['auditbeat-*', 'endgame-*', 'filebeat-*', 'logs-*', 'packetbeat-*', 'winlogbeat-*'], + data_sources: [ + ['auditbeat-*', 'endgame-*', 'filebeat-*', 'logs-*', 'packetbeat-*', 'winlogbeat-*'], + ], + }, + ], +}; +export const DEFAULT_TRANSFORMS_SETTING = JSON.stringify(defaultTransformsSetting, null, 2); + /** * Id for the signals alerting type */ @@ -214,3 +248,10 @@ export const showAllOthersBucket: string[] = [ 'destination.ip', 'user.name', ]; + +/** + * Used for transforms for metrics_entities. If the security_solutions pulls in + * the metrics_entities plugin, then it should pull this constant from there rather + * than use it from here. + */ +export const ELASTIC_NAME = 'estc'; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 70ed468e61554..8d1cc4ca2c1f0 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -13,6 +13,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; */ const allowedExperimentalValues = Object.freeze({ trustedAppsByPolicyEnabled: false, + metricsEntitiesEnabled: false, eventFilteringEnabled: false, hostIsolationEnabled: false, }); diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts index 0905fc052d1a9..a000fcf6136e5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts @@ -72,9 +72,13 @@ export interface AuthenticationBucket { doc_count: number; failures: { doc_count: number; + // TODO: Keep this or make a new structure? + value?: number; }; successes: { doc_count: number; + // TODO: Keep this or make a new structure? + value?: number; }; authentication: { hits: { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index 3926fdc72f73a..bae99649c2e01 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -16,9 +16,11 @@ export * from './uncommon_processes'; export enum HostsQueries { authentications = 'authentications', + authenticationsEntities = 'authenticationsEntities', details = 'hostDetails', firstOrLastSeen = 'firstOrLastSeen', hosts = 'hosts', + hostsEntities = 'hostsEntities', overview = 'overviewHost', uncommonProcesses = 'uncommonProcesses', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/authentications/index.ts index 4eb8af02af355..81e1945dcd010 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/authentications/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/authentications/index.ts @@ -12,6 +12,8 @@ import { HostsKpiHistogramData } from '../common'; export interface HostsKpiAuthenticationsHistogramCount { doc_count: number; + // TODO: Should I keep this or split this interface into two for entities and non-entities? + value?: number; } export type HostsKpiAuthenticationsRequestOptions = RequestBasicOptions; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts index 79054fc736a80..d48172bebee4c 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts @@ -16,8 +16,11 @@ import { HostsKpiUniqueIpsStrategyResponse } from './unique_ips'; export enum HostsKpiQueries { kpiAuthentications = 'hostsKpiAuthentications', + kpiAuthenticationsEntities = 'hostsKpiAuthenticationsEntities', kpiHosts = 'hostsKpiHosts', + kpiHostsEntities = 'hostsKpiHostsEntities', kpiUniqueIps = 'hostsKpiUniqueIps', + kpiUniqueIpsEntities = 'hostsKpiUniqueIpsEntities', } export type HostsKpiStrategyResponse = diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index ae2cff20717f3..936d9c360afb0 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -61,6 +61,7 @@ import { } from './network'; import { MatrixHistogramQuery, + MatrixHistogramQueryEntities, MatrixHistogramRequestOptions, MatrixHistogramStrategyResponse, } from './matrix_histogram'; @@ -75,7 +76,8 @@ export type FactoryQueryTypes = | HostsKpiQueries | NetworkQueries | NetworkKpiQueries - | typeof MatrixHistogramQuery; + | typeof MatrixHistogramQuery + | typeof MatrixHistogramQueryEntities; export interface RequestBasicOptions extends IEsSearchRequest { timerange: TimerangeInput; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts index 81edb51e41458..fd1cf32e21400 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -23,9 +23,11 @@ export * from './dns'; export * from './events'; export const MatrixHistogramQuery = 'matrixHistogram'; +export const MatrixHistogramQueryEntities = 'matrixHistogramEntities'; export enum MatrixHistogramType { authentications = 'authentications', + authenticationsEntities = 'authenticationsEntities', anomalies = 'anomalies', events = 'events', alerts = 'alerts', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts index 24c6484f94e71..2e0a5d7d2f0f1 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts @@ -23,6 +23,8 @@ export enum NetworkQueries { overview = 'overviewNetwork', tls = 'tls', topCountries = 'topCountries', + topCountriesEntities = 'topCountriesEntities', topNFlow = 'topNFlow', + topNFlowEntities = 'topNFlowEntities', users = 'users', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/kpi/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/kpi/index.ts index fa9e55096f7a6..cb18a3edb4937 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/kpi/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/kpi/index.ts @@ -19,10 +19,14 @@ import { NetworkKpiUniquePrivateIpsStrategyResponse } from './unique_private_ips export enum NetworkKpiQueries { dns = 'networkKpiDns', + dnsEntities = 'networkKpiDnsEntities', networkEvents = 'networkKpiNetworkEvents', + networkEventsEntities = 'networkKpiNetworkEventsEntities', tlsHandshakes = 'networkKpiTlsHandshakes', + tlsHandshakesEntities = 'networkKpiTlsHandshakesEntities', uniqueFlows = 'networkKpiUniqueFlows', uniquePrivateIps = 'networkKpiUniquePrivateIps', + uniquePrivateIpsEntities = 'networkKpiUniquePrivateIpsEntities', } export type NetworkKpiStrategyResponse = diff --git a/x-pack/plugins/security_solution/common/transforms/types.ts b/x-pack/plugins/security_solution/common/transforms/types.ts new file mode 100644 index 0000000000000..ac4e3cae92e22 --- /dev/null +++ b/x-pack/plugins/security_solution/common/transforms/types.ts @@ -0,0 +1,39 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +/** + * Kibana configuration schema + */ +export const transformConfigSchema = schema.object({ + auto_start: schema.boolean(), + auto_create: schema.boolean(), + enabled: schema.boolean(), + query: schema.maybe(schema.object({}, { unknowns: 'allow' })), + retention_policy: schema.maybe( + schema.object({ + time: schema.object({ + field: schema.string(), + max_age: schema.string(), + }), + }) + ), + docs_per_second: schema.maybe(schema.number({ min: 1 })), + max_page_search_size: schema.maybe(schema.number({ min: 1, max: 10000 })), + settings: schema.arrayOf( + schema.object({ + prefix: schema.string(), + indices: schema.arrayOf(schema.string()), + data_sources: schema.arrayOf(schema.arrayOf(schema.string())), + disable_widgets: schema.maybe(schema.arrayOf(schema.string())), + disable_transforms: schema.maybe(schema.arrayOf(schema.string())), + }) + ), +}); + +export type TransformConfigSchema = TypeOf; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx new file mode 100644 index 0000000000000..7da514a551037 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ReactWrapper } from 'enzyme'; +import React from 'react'; +import { getColumns } from './columns'; +import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; +import { mockBrowserFields } from '../../containers/source/mock'; +import { EventFieldsData } from './types'; + +interface Column { + field: string; + name: string; + sortable: boolean; + render: (field: string, data: EventFieldsData) => JSX.Element; +} + +describe('getColumns', () => { + const mount = useMountAppended(); + const defaultProps = { + browserFields: mockBrowserFields, + columnHeaders: [], + contextId: 'some-context', + eventId: 'some-event', + getLinkValue: jest.fn(), + onUpdateColumns: jest.fn(), + timelineId: 'some-timeline', + toggleColumn: jest.fn(), + }; + + test('should have expected fields', () => { + const columns = getColumns(defaultProps); + columns.forEach((column) => { + expect(column).toHaveProperty('field'); + expect(column).toHaveProperty('name'); + expect(column).toHaveProperty('render'); + expect(column).toHaveProperty('sortable'); + }); + }); + + describe('column checkbox', () => { + let checkboxColumn: Column; + const mockDataToUse = mockBrowserFields.agent; + const testData = { + type: 'someType', + category: 'agent', + ...mockDataToUse, + } as EventFieldsData; + + beforeEach(() => { + checkboxColumn = getColumns(defaultProps)[0] as Column; + }); + + test('should be disabled when the field does not exist', () => { + const testField = 'nonExistingField'; + const wrapper = mount( + {checkboxColumn.render(testField, testData)} + ) as ReactWrapper; + expect( + wrapper.find(`[data-test-subj="toggle-field-${testField}"]`).first().prop('disabled') + ).toBe(true); + }); + + test('should be enabled when the field does exist', () => { + const testField = mockDataToUse.fields + ? Object.keys(mockDataToUse.fields)[0] + : 'agent.hostname'; + const wrapper = mount( + {checkboxColumn.render(testField, testData)} + ) as ReactWrapper; + expect( + wrapper.find(`[data-test-subj="toggle-field-${testField}"]`).first().prop('disabled') + ).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 22c2b40ed62ce..3a891222c11a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -15,10 +15,12 @@ import { EuiPanel, EuiToolTip, EuiIconTip, + EuiText, } from '@elastic/eui'; +import { get, isEmpty } from 'lodash'; +import memoizeOne from 'memoize-one'; import React from 'react'; import styled from 'styled-components'; - import { onFocusReFocusDraggable } from '../accessibility/helpers'; import { BrowserFields } from '../../containers/source'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; @@ -58,7 +60,10 @@ const FullWidthFlexGroup = styled(EuiFlexGroup)` const FullWidthFlexItem = styled(EuiFlexItem)` width: 100%; `; - +export const getFieldFromBrowserField = memoizeOne( + (keys: string[], browserFields: BrowserFields): BrowserFields => get(browserFields, keys), + (newArgs, lastArgs) => newArgs[0].join() === lastArgs[0].join() +); export const getColumns = ({ browserFields, columnHeaders, @@ -86,6 +91,10 @@ export const getColumns = ({ width: '30px', render: (field: string, data: EventFieldsData) => { const label = data.isObjectArray ? i18n.NESTED_COLUMN(field) : i18n.VIEW_COLUMN(field); + const fieldFromBrowserField = getFieldFromBrowserField( + [data.category, 'fields', field], + browserFields + ); return ( ); @@ -112,62 +123,69 @@ export const getColumns = ({ name: i18n.FIELD, sortable: true, truncateText: false, - render: (field: string, data: EventFieldsData) => ( - - - - - - - - - {data.isObjectArray && data.type !== 'geo_point' ? ( - <>{field} - ) : ( - ( -
- - - -
- )} - > - { + const fieldFromBrowserField = getFieldFromBrowserField( + [data.category, 'fields', field], + browserFields + ); + return ( + + + + + + + + {(data.isObjectArray && data.type !== 'geo_point') || fieldFromBrowserField == null ? ( + {field} + ) : ( + ( +
+ + + +
+ )} + > + +
+ )} +
+ {!isEmpty(data.description) && ( + + -
+
)} -
- - - - - ), + + ); + }, }, { field: 'values', diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index e6e868f1a7365..c99275ec49ab3 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -44,6 +44,10 @@ import { RowRenderer } from '../../../timelines/components/timeline/body/rendere import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; +import { + defaultControlColumn, + ControlColumnProps, +} from '../../../timelines/components/timeline/body/control_columns'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const UTILITY_BAR_HEIGHT = 19; // px @@ -273,6 +277,9 @@ const EventsViewerComponent: React.FC = ({ setIsQueryLoading(loading); }, [loading]); + const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; + const trailingControlColumns: ControlColumnProps[] = []; + return ( = ({ itemsCount: totalCountMinusDeleted, itemsPerPage, })} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} />